@seliseblocks/mailcraft 0.1.1 → 0.2.0

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 (69) hide show
  1. package/DOCS.md +238 -10
  2. package/README.md +11 -3
  3. package/README.md.txt +134 -0
  4. package/dist/mailcraft-editor.bundle.js +51 -39
  5. package/dist/mailcraft-editor.bundle.js.map +3 -3
  6. package/examples/templates/order-confirmed.html +80 -80
  7. package/examples/vanilla.html +234 -22
  8. package/package.json +7 -2
  9. package/src/core/assets.js +10 -15
  10. package/src/core/binder.js +120 -118
  11. package/src/core/blocks.js +14 -1
  12. package/src/core/css-cascade.js +117 -117
  13. package/src/core/editor-core.js +1530 -1492
  14. package/src/core/export.js +142 -55
  15. package/src/core/i18n/ar.js +219 -177
  16. package/src/core/i18n/bg.js +196 -152
  17. package/src/core/i18n/bn.js +218 -176
  18. package/src/core/i18n/ca.js +196 -152
  19. package/src/core/i18n/cs.js +196 -152
  20. package/src/core/i18n/da.js +196 -152
  21. package/src/core/i18n/de-CH.js +196 -152
  22. package/src/core/i18n/de.js +196 -152
  23. package/src/core/i18n/dz.js +221 -179
  24. package/src/core/i18n/el.js +196 -152
  25. package/src/core/i18n/en.js +3 -10
  26. package/src/core/i18n/es.js +196 -152
  27. package/src/core/i18n/et.js +196 -152
  28. package/src/core/i18n/fi.js +196 -152
  29. package/src/core/i18n/fr.js +196 -152
  30. package/src/core/i18n/hr.js +196 -152
  31. package/src/core/i18n/hu.js +196 -152
  32. package/src/core/i18n/index.js +83 -83
  33. package/src/core/i18n/it.js +196 -152
  34. package/src/core/i18n/lt.js +196 -152
  35. package/src/core/i18n/lv.js +196 -152
  36. package/src/core/i18n/nb.js +196 -152
  37. package/src/core/i18n/nl.js +196 -152
  38. package/src/core/i18n/pl.js +196 -152
  39. package/src/core/i18n/pt.js +196 -152
  40. package/src/core/i18n/ro.js +196 -152
  41. package/src/core/i18n/ru.js +196 -152
  42. package/src/core/i18n/sk.js +196 -152
  43. package/src/core/i18n/sl.js +196 -152
  44. package/src/core/i18n/sv.js +196 -152
  45. package/src/core/i18n/tables.js +50 -50
  46. package/src/core/i18n/tr.js +196 -152
  47. package/src/core/i18n/uk.js +196 -152
  48. package/src/core/icons.js +237 -235
  49. package/src/core/ids.js +1 -1
  50. package/src/core/import-html.js +1025 -959
  51. package/src/core/layout-style.js +100 -100
  52. package/src/core/parse.js +10 -10
  53. package/src/core/placeholder.js +15 -15
  54. package/src/core/sanitize.js +141 -141
  55. package/src/core/storage-limits.js +184 -184
  56. package/src/core/storage.js +85 -85
  57. package/src/core/theme.js +1 -1
  58. package/src/core/variables.js +11 -11
  59. package/src/index.js +9 -9
  60. package/src/mailcraft-editor.js +26 -14
  61. package/src/render/block-body.js +49 -6
  62. package/src/render/canvas.js +31 -2
  63. package/src/render/fields.js +602 -588
  64. package/src/render/focus-preserve.js +158 -158
  65. package/src/render/rte.js +241 -212
  66. package/src/render/screenshot.js +132 -132
  67. package/src/render/story.js +415 -415
  68. package/src/render/style.js +8 -0
  69. package/types/index.d.ts +419 -0
@@ -1,58 +1,145 @@
1
- /**
2
- * Ported verbatim from the original `buildHtml()`, including its defining
3
- * trait: it does not re-template each block from `props` a second time, it
4
- * reads back the already-rendered live DOM node for each block (`grab`) and
5
- * strips editor-only attributes. The one adaptation: the original queried
6
- * plain `document` (the whole prototype ran outside any shadow tree); this
7
- * package renders inside a Shadow DOM, so `grab` is parameterized on the root
8
- * to search (the element's `shadowRoot`) instead of hardcoding `document`.
9
- */
10
-
11
- import { cssUrl } from './sanitize.js';
1
+ /**
2
+ * Ported verbatim from the original `buildHtml()`, including its defining
3
+ * trait: it does not re-template each block from `props` a second time, it
4
+ * reads back the already-rendered live DOM node for each block (`grab`) and
5
+ * strips editor-only attributes. The one adaptation: the original queried
6
+ * plain `document` (the whole prototype ran outside any shadow tree); this
7
+ * package renders inside a Shadow DOM, so `grab` is parameterized on the root
8
+ * to search (the element's `shadowRoot`) instead of hardcoding `document`.
9
+ */
10
+
11
+ import { cssUrl } from './sanitize.js';
12
12
  import { rowBorderCss, rowMargin, rowPad } from './layout-style.js';
13
-
14
- export function buildHtml(state, root, boxCss) {
15
- const d = state.doc; const t = d.theme;
16
- const grab = (id) => {
17
- const el = root.querySelector('[data-mc-content="' + id + '"]');
18
- if (!el) return '';
19
- return el.outerHTML
20
- .replace(/\scontenteditable="[^"]*"/g, '')
21
- .replace(/\sdata-mc-[a-z-]+="[^"]*"/g, '')
22
- .replace(/\sspellcheck="[^"]*"/g, '')
23
- .replace(/\sdraggable="[^"]*"/g, '');
24
- };
25
- const rows = d.rows.map((r) => {
26
- const rp = r.props;
27
- const colInner = (c) => c.blocks.map((b) => {
28
- if (b.type === 'css') return '<style>' + (b.props.code || '') + '</style>';
29
- if (b.type === 'html') return b.props.code || '';
30
- if (b.type === 'svg') return '<div style="text-align:' + b.props.align + ';padding:' + b.props.py + 'px 0">' + (b.props.code || '') + '</div>';
31
- return '<div style="' + boxCss(b.props) + '">' + grab(b.id) + '</div>';
32
- }).join('\n ') || '&nbsp;';
33
- const cells = r.cols.map((c) => {
34
- // Column-level styling (bg/radius/inner padding) renders as a wrapper
35
- // <div> inside the cell so the gutter padding stays unpainted --
36
- // mirrors render/canvas.js's `host` wrapper.
37
- const inner = (c.bg || c.border || c.radius || c.padY || c.padX)
38
- ? '<div style="background:' + (c.bg || 'transparent') + ';' + (c.border ? 'border:' + c.border + 'px ' + (c.borderStyle || 'solid') + ' ' + (c.lineColor || '#e2e2e5') + ';' : '') + 'border-radius:' + (c.radius || 0) + 'px;padding:' + (c.padY || 0) + 'px ' + (c.padX || 0) + 'px">\n ' + colInner(c) + '\n </div>'
39
- : colInner(c);
40
- return '<td width="' + c.span + '%" valign="' + rp.valign + '" style="padding:0 ' + Math.round(rp.gap / 2) + 'px;">\n ' + inner + '\n </td>';
41
- }).join('\n ');
42
- const cssBody = rp.layout === 'grid'
43
- ? '<div style="display:grid;grid-template-columns:repeat(' + (rp.gridCols || 2) + ',minmax(0,1fr));gap:' + rp.gap + 'px">\n ' + r.cols.map((c) => '<div>' + colInner(c) + '</div>').join('\n ') + '\n </div>'
44
- : '<div style="display:flex;flex-direction:' + (rp.flexDir || 'row') + ';justify-content:' + (rp.justify || 'flex-start') + ';align-items:' + (rp.alignItems || 'stretch') + ';flex-wrap:' + (rp.wrap ? 'wrap' : 'nowrap') + ';gap:' + rp.gap + 'px">\n ' + r.cols.map((c) => '<div style="flex:' + c.span + ' 1 auto;min-width:0">' + colInner(c) + '</div>').join('\n ') + '\n </div>';
45
- const body = rp.layout && rp.layout !== 'columns'
46
- ? cssBody
47
- : '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>\n ' + cells + '\n </tr></table>';
48
- const tdBg = rp.bgImage
49
- ? 'background-color:' + (rp.bg || t.contentBg) + ';background-image:' + (rp.overlay ? 'linear-gradient(rgba(20,22,24,' + (rp.overlay / 100) + '),rgba(20,22,24,' + (rp.overlay / 100) + ')),' : '') + 'url(&quot;' + cssUrl(rp.bgImage) + '&quot;);background-size:' + (rp.bgSize || 'cover') + ';background-position:' + (rp.bgPos || 'center') + ';background-repeat:' + (rp.bgRepeat || 'no-repeat') + ';'
50
- : 'background:' + (rp.bg || t.contentBg) + ';';
51
- const tdBorder = rowBorderCss(rp)
52
- + (rp.radius ? 'border-radius:' + rp.radius + 'px;' : '')
13
+
14
+ const LOGIC_OPEN = { condition: '{{#if ', loop: '{{#each ' };
15
+ const LOGIC_CLOSE = { condition: '{{/if}}', loop: '{{/each}}' };
16
+
17
+ /**
18
+ * What each dynamic-content marker block actually emits, decided over the
19
+ * whole document in reading order so the output template is always balanced
20
+ * no matter how the markers were arranged or mangled: an end with no open of
21
+ * its kind on top of the stack emits nothing, and opens still unclosed after
22
+ * the last row are closed in `tail`, appended after the final row. Markers
23
+ * emit literal template tags -- like merge tags, the editor never evaluates
24
+ * them; the host's engine runs before any mail client parses the HTML, so raw
25
+ * {{#each}} text in the markup is the standard pattern, not invalid HTML.
26
+ * Angle brackets are stripped from expressions: no Handlebars-style
27
+ * expression contains them, and it keeps a typed '<' from opening a tag in
28
+ * the sent document.
29
+ */
30
+ function logicPlan(doc) {
31
+ const expr = (v) => String(v || '').replace(/[<>]/g, '').trim();
32
+ const emit = new Map();
33
+ const stack = [];
34
+ doc.rows.forEach((r) => r.cols.forEach((c) => c.blocks.forEach((b) => {
35
+ if (b.type !== 'condition' && b.type !== 'loop') return;
36
+ if (!b.props.end) {
37
+ const e = expr(b.props.expr);
38
+ emit.set(b.id, e ? LOGIC_OPEN[b.type] + e + '}}' : '');
39
+ if (e) stack.push(b.type);
40
+ } else if (stack[stack.length - 1] === b.type) {
41
+ stack.pop();
42
+ emit.set(b.id, LOGIC_CLOSE[b.type]);
43
+ } else {
44
+ emit.set(b.id, '');
45
+ }
46
+ })));
47
+ const tail = stack.reverse().map((k) => LOGIC_CLOSE[k]).join('\n ');
48
+ return { emit, tail };
49
+ }
50
+
51
+ /**
52
+ * Display-only dressing for the Code modal's live-preview iframe: the
53
+ * exported template carries literal {{#if}}/{{#each}} tags, which an iframe
54
+ * renders as bare text at odd positions (a browser even foster-parents the
55
+ * between-row ones out of the table entirely). For *viewing*, each tag
56
+ * becomes the same dashed band the canvas draws -- tags between <tr>s become
57
+ * a slim full-width row so they hold their place in the table, tags inside a
58
+ * cell become an inline chip. Never applied to the HTML the host receives:
59
+ * exportHtml/copy/download all carry the real tags.
60
+ */
61
+ export function decorateLogicTags(html) {
62
+ const esc = (v) => String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
63
+ const chip = (kind, expr, end) => {
64
+ const color = kind === 'each' ? '#7c3aed' : '#0e7490';
65
+ const word = end ? (kind === 'each' ? 'END LOOP' : 'END IF') : (kind === 'each' ? 'REPEAT EACH' : 'SHOW IF');
66
+ return '<span style="display:inline-flex;align-items:center;gap:7px;box-sizing:border-box;border:1.5px dashed ' + color + ';border-radius:7px;background:' + color + '14;padding:4px 10px;margin:2px 0;color:' + color + ';font-family:ui-monospace,monospace;font-size:9.5px;font-weight:700;letter-spacing:0.12em;">'
67
+ + (end ? '⏶ ' : '⏷ ') + word
68
+ + (expr ? ' <span style="font-weight:400;font-size:11px;letter-spacing:0;">{{ ' + esc(expr) + ' }}</span>' : '')
69
+ + '</span>';
70
+ };
71
+ const bandRow = (kind, expr, end) => '<tr><td colspan="99" style="padding:2px 8px;">' + chip(kind, expr, end) + '</td></tr>';
72
+ let s = String(html || '');
73
+ // Between-row tags first (adjacent to a <tr> or after a </tr>), looped for
74
+ // the same adjacency reason as import-html's foldLogicWrappers.
75
+ const parse = (tag) => { const m = tag.match(/^#(if|each)\s+(.+)$/); return m ? [m[1], m[2].trim(), false] : [tag.slice(1), '', true]; };
76
+ for (let i = 0; i < 6; i++) {
77
+ const before = s;
78
+ s = s.replace(/\{\{(#(?:if|each)\s+[^{}]+?|\/(?:if|each))\s*\}\}(?=\s*(?:<tr[\s>]|<\/table))/gi, (m, tag) => bandRow(...parse(tag)));
79
+ s = s.replace(/(<\/tr>\s*)\{\{(#(?:if|each)\s+[^{}]+?|\/(?:if|each))\s*\}\}/gi, (m, pre, tag) => pre + bandRow(...parse(tag)));
80
+ if (s === before) break;
81
+ }
82
+ // Whatever remains sits inside a cell (or prose): inline chips in place.
83
+ s = s.replace(/\{\{#(if|each)\s+([^{}]+?)\s*\}\}/gi, (m, kind, expr) => chip(kind.toLowerCase(), expr, false));
84
+ s = s.replace(/\{\{\/(if|each)\s*\}\}/gi, (m, kind) => chip(kind.toLowerCase(), '', true));
85
+ return s;
86
+ }
87
+
88
+ export function buildHtml(state, root, boxCss) {
89
+ const d = state.doc; const t = d.theme;
90
+ const logic = logicPlan(d);
91
+ const grab = (id) => {
92
+ const el = root.querySelector('[data-mc-content="' + id + '"]');
93
+ if (!el) return '';
94
+ return el.outerHTML
95
+ .replace(/\scontenteditable="[^"]*"/g, '')
96
+ .replace(/\sdata-mc-[a-z-]+="[^"]*"/g, '')
97
+ .replace(/\sspellcheck="[^"]*"/g, '')
98
+ .replace(/\sdata-(?:gramm|gramm_editor|enable-grammarly|lt-active)="[^"]*"/g, '')
99
+ .replace(/\sdraggable="[^"]*"/g, '');
100
+ };
101
+ const rows = d.rows.map((r) => {
102
+ const rp = r.props;
103
+ // A row holding nothing but logic markers exists to wrap the *sections*
104
+ // around it (drop a Condition onto the canvas above and below a group of
105
+ // rows). Emitting its <tr> scaffolding would leave an empty padded band
106
+ // in the sent email, so only the tags themselves are emitted -- they sit
107
+ // between <tr>s as plain text, which the host's engine consumes before
108
+ // any mail client parses the markup.
109
+ const blocksOf = r.cols.reduce((a, c) => a.concat(c.blocks), []);
110
+ if (blocksOf.length && blocksOf.every((b) => b.type === 'condition' || b.type === 'loop')) {
111
+ return blocksOf.map((b) => logic.emit.get(b.id) || '').filter(Boolean).map((s) => ' ' + s).join('\n');
112
+ }
113
+ const colInner = (c) => c.blocks.map((b) => {
114
+ if (b.type === 'css') return '<style>' + (b.props.code || '') + '</style>';
115
+ if (b.type === 'html') return b.props.code || '';
116
+ if (b.type === 'svg') return '<div style="text-align:' + b.props.align + ';padding:' + b.props.py + 'px 0">' + (b.props.code || '') + '</div>';
117
+ if (b.type === 'condition' || b.type === 'loop') return logic.emit.get(b.id) || '';
118
+ return '<div style="' + boxCss(b.props) + '">' + grab(b.id) + '</div>';
119
+ }).filter(Boolean).join('\n ') || '&nbsp;';
120
+ const cells = r.cols.map((c) => {
121
+ // Column-level styling (bg/radius/inner padding) renders as a wrapper
122
+ // <div> inside the cell so the gutter padding stays unpainted --
123
+ // mirrors render/canvas.js's `host` wrapper.
124
+ const inner = (c.bg || c.border || c.radius || c.padY || c.padX)
125
+ ? '<div style="background:' + (c.bg || 'transparent') + ';' + (c.border ? 'border:' + c.border + 'px ' + (c.borderStyle || 'solid') + ' ' + (c.lineColor || '#e2e2e5') + ';' : '') + 'border-radius:' + (c.radius || 0) + 'px;padding:' + (c.padY || 0) + 'px ' + (c.padX || 0) + 'px">\n ' + colInner(c) + '\n </div>'
126
+ : colInner(c);
127
+ return '<td width="' + c.span + '%" valign="' + rp.valign + '" style="padding:0 ' + Math.round(rp.gap / 2) + 'px;">\n ' + inner + '\n </td>';
128
+ }).join('\n ');
129
+ const cssBody = rp.layout === 'grid'
130
+ ? '<div style="display:grid;grid-template-columns:repeat(' + (rp.gridCols || 2) + ',minmax(0,1fr));gap:' + rp.gap + 'px">\n ' + r.cols.map((c) => '<div>' + colInner(c) + '</div>').join('\n ') + '\n </div>'
131
+ : '<div style="display:flex;flex-direction:' + (rp.flexDir || 'row') + ';justify-content:' + (rp.justify || 'flex-start') + ';align-items:' + (rp.alignItems || 'stretch') + ';flex-wrap:' + (rp.wrap ? 'wrap' : 'nowrap') + ';gap:' + rp.gap + 'px">\n ' + r.cols.map((c) => '<div style="flex:' + c.span + ' 1 auto;min-width:0">' + colInner(c) + '</div>').join('\n ') + '\n </div>';
132
+ const body = rp.layout && rp.layout !== 'columns'
133
+ ? cssBody
134
+ : '<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>\n ' + cells + '\n </tr></table>';
135
+ const tdBg = rp.bgImage
136
+ ? 'background-color:' + (rp.bg || t.contentBg) + ';background-image:' + (rp.overlay ? 'linear-gradient(rgba(20,22,24,' + (rp.overlay / 100) + '),rgba(20,22,24,' + (rp.overlay / 100) + ')),' : '') + 'url(&quot;' + cssUrl(rp.bgImage) + '&quot;);background-size:' + (rp.bgSize || 'cover') + ';background-position:' + (rp.bgPos || 'center') + ';background-repeat:' + (rp.bgRepeat || 'no-repeat') + ';'
137
+ : 'background:' + (rp.bg || t.contentBg) + ';';
138
+ const tdBorder = rowBorderCss(rp)
139
+ + (rp.radius ? 'border-radius:' + rp.radius + 'px;' : '')
53
140
  + (rp.shadow ? 'box-shadow:' + rp.shadow + ';' : '')
54
141
  + ((rp.mt || rp.mr || rp.mb || rp.ml || rp.my) ? 'margin:' + rowMargin(rp) + ';' : '');
55
- return ' <tr>\n <td style="padding:' + rowPad(rp) + ';' + tdBg + tdBorder + '">\n ' + body + '\n </td>\n </tr>';
56
- }).join('\n');
57
- return '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width,initial-scale=1">\n<meta name="color-scheme" content="light dark">\n<title>' + 'Email' + '</title>\n</head>\n<body style="margin:0;padding:0;background:' + t.bg + ';font-family:' + t.font.replace(/"/g, "'") + ';color:' + t.text + ';-webkit-font-smoothing:antialiased;">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:' + t.bg + ';">\n <tr><td align="center" style="padding:24px 12px;">\n <table role="presentation" width="' + t.width + '" cellpadding="0" cellspacing="0" border="0" style="width:' + t.width + 'px;max-width:100%;background:' + t.contentBg + ';">\n' + rows + '\n </table>\n </td></tr>\n</table>\n</body>\n</html>';
58
- }
142
+ return ' <tr>\n <td style="padding:' + rowPad(rp) + ';' + tdBg + tdBorder + '">\n ' + body + '\n </td>\n </tr>';
143
+ }).join('\n') + (logic.tail ? '\n ' + logic.tail : '');
144
+ return '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width,initial-scale=1">\n<meta name="color-scheme" content="light dark">\n<title>' + 'Email' + '</title>\n</head>\n<body style="margin:0;padding:0;background:' + t.bg + ';font-family:' + t.font.replace(/"/g, "'") + ';color:' + t.text + ';-webkit-font-smoothing:antialiased;">\n<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:' + t.bg + ';">\n <tr><td align="center" style="padding:24px 12px;">\n <table role="presentation" width="' + t.width + '" cellpadding="0" cellspacing="0" border="0" style="width:' + t.width + 'px;max-width:100%;background:' + t.contentBg + ';">\n' + rows + '\n </table>\n </td></tr>\n</table>\n</body>\n</html>';
145
+ }