@maily-to/shared 0.0.1 → 2.0.0-beta.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/LICENSE +5 -5
- package/dist/index.cjs +1081 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1040 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +1006 -80
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +922 -131
- package/dist/index.mjs.map +1 -1
- package/package.json +31 -26
- package/readme.md +4 -4
- package/dist/index.d.ts +0 -114
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -213
- package/dist/index.js.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/alignment.ts
|
|
3
|
+
const TEXT_ALIGNMENTS = {
|
|
4
|
+
LEFT: "left",
|
|
5
|
+
CENTER: "center",
|
|
6
|
+
RIGHT: "right"
|
|
7
|
+
};
|
|
8
|
+
const allowedTextAligns = Object.freeze(Object.values(TEXT_ALIGNMENTS));
|
|
9
|
+
const DEFAULT_TEXT_ALIGN = TEXT_ALIGNMENTS.LEFT;
|
|
10
|
+
function isAllowedTextAlignment(value) {
|
|
11
|
+
return typeof value === "string" && allowedTextAligns.includes(value);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/field-mode.ts
|
|
15
|
+
/**
|
|
16
|
+
* Controls how multi-value fields (padding, margin, border) are edited.
|
|
17
|
+
* "none" disables the field, "uniform" links all sides to one value,
|
|
18
|
+
* "mixed" allows independent per-side values, "preset" uses predefined
|
|
19
|
+
* size options (e.g. small/medium/large).
|
|
20
|
+
*/
|
|
21
|
+
const FIELD_MODE = {
|
|
22
|
+
NONE: "none",
|
|
23
|
+
MIXED: "mixed",
|
|
24
|
+
UNIFORM: "uniform",
|
|
25
|
+
PRESET: "preset"
|
|
26
|
+
};
|
|
27
|
+
const allowedFieldModes = Object.values(FIELD_MODE);
|
|
28
|
+
function isAllowedFieldMode(mode) {
|
|
29
|
+
return typeof mode === "string" && allowedFieldModes.includes(mode);
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/border.ts
|
|
33
|
+
const BORDER_STYLES = {
|
|
34
|
+
SOLID: "solid",
|
|
35
|
+
DASHED: "dashed",
|
|
36
|
+
DOTTED: "dotted"
|
|
37
|
+
};
|
|
38
|
+
const DEFAULT_BORDER_STYLE = BORDER_STYLES.SOLID;
|
|
39
|
+
const DEFAULT_BORDER_COLOR = "#000000";
|
|
40
|
+
/**
|
|
41
|
+
* Converts a BorderStyleConfig into CSS properties for inline styles.
|
|
42
|
+
* When all widths or radii are equal (or the mode is "uniform"), emits
|
|
43
|
+
* shorthand CSS; otherwise emits per-side longhand properties.
|
|
44
|
+
*/
|
|
45
|
+
function getBorderStyle(config) {
|
|
46
|
+
const { borderStyle = DEFAULT_BORDER_STYLE, borderColor = DEFAULT_BORDER_COLOR, borderWidthMode, borderTopWidth, borderRightWidth, borderBottomWidth, borderLeftWidth, borderRadiusMode, borderTopLeftRadius, borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius } = config;
|
|
47
|
+
const isBorderWidthUniform = borderWidthMode === FIELD_MODE.UNIFORM || borderLeftWidth === borderRightWidth && borderRightWidth === borderBottomWidth && borderBottomWidth === borderTopWidth;
|
|
48
|
+
const isBorderRadiusUniform = borderRadiusMode === FIELD_MODE.UNIFORM || borderTopLeftRadius === borderTopRightRadius && borderTopRightRadius === borderBottomRightRadius && borderBottomRightRadius === borderBottomLeftRadius;
|
|
49
|
+
return {
|
|
50
|
+
...isBorderWidthUniform ? { border: `${borderTopWidth}px ${borderStyle} ${borderColor}` } : {
|
|
51
|
+
borderTopWidth: `${borderTopWidth}px`,
|
|
52
|
+
borderRightWidth: `${borderRightWidth}px`,
|
|
53
|
+
borderBottomWidth: `${borderBottomWidth}px`,
|
|
54
|
+
borderLeftWidth: `${borderLeftWidth}px`,
|
|
55
|
+
borderStyle,
|
|
56
|
+
borderColor
|
|
57
|
+
},
|
|
58
|
+
...isBorderRadiusUniform ? { borderRadius: `${borderTopLeftRadius}px` } : { borderRadius: `${borderTopLeftRadius}px ${borderTopRightRadius}px ${borderBottomRightRadius}px ${borderBottomLeftRadius}px` }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/button-kind.ts
|
|
63
|
+
/**
|
|
64
|
+
* Button layout kinds.
|
|
65
|
+
* "tight" renders the button at its content width,
|
|
66
|
+
* "full-width" stretches it to fill the container.
|
|
67
|
+
*/
|
|
68
|
+
const BUTTON_KINDS = {
|
|
69
|
+
TIGHT: "tight",
|
|
70
|
+
FULL_WIDTH: "full-width"
|
|
71
|
+
};
|
|
72
|
+
const allowedButtonKinds = Object.freeze(Object.values(BUTTON_KINDS));
|
|
73
|
+
const DEFAULT_BUTTON_KIND = BUTTON_KINDS.TIGHT;
|
|
74
|
+
function isAllowedButtonKind(value) {
|
|
75
|
+
return typeof value === "string" && allowedButtonKinds.includes(value);
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/constants.ts
|
|
79
|
+
/** HTML data attribute key used to identify node types in rendered output. */
|
|
80
|
+
const DATA_NODE_TYPE_KEY = "data-node-type";
|
|
81
|
+
/** HTML data attribute key used to store serialized visibility rules. */
|
|
82
|
+
const DATA_VISIBILITY_RULE_KEY = "data-visibility-rule";
|
|
83
|
+
/** Prefix prepended to all Maily error messages for easy identification. */
|
|
84
|
+
const MAILY_ERROR_PREFIX = "[Maily Error]";
|
|
85
|
+
/** URL for filing bug reports against the Maily repository. */
|
|
86
|
+
const MAILY_BUG_REPORT_URL = "https://github.com/arikchakma/maily.to/issues/new";
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/font.ts
|
|
89
|
+
/**
|
|
90
|
+
* List of allowed fallback font families. These are system fonts or
|
|
91
|
+
* generic font families that serve as fallbacks when a web font
|
|
92
|
+
* fails to load.
|
|
93
|
+
*/
|
|
94
|
+
const allowedFallbackFonts = [
|
|
95
|
+
"Arial",
|
|
96
|
+
"Helvetica",
|
|
97
|
+
"Verdana",
|
|
98
|
+
"Georgia",
|
|
99
|
+
"Times New Roman",
|
|
100
|
+
"serif",
|
|
101
|
+
"sans-serif",
|
|
102
|
+
"monospace",
|
|
103
|
+
"cursive",
|
|
104
|
+
"fantasy"
|
|
105
|
+
];
|
|
106
|
+
const allowedFontFormats = [
|
|
107
|
+
"woff",
|
|
108
|
+
"woff2",
|
|
109
|
+
"truetype",
|
|
110
|
+
"opentype",
|
|
111
|
+
"embedded-opentype",
|
|
112
|
+
"svg"
|
|
113
|
+
];
|
|
114
|
+
/**
|
|
115
|
+
* Default font configuration: Inter with sans-serif fallback,
|
|
116
|
+
* loaded from the Maily CDN as woff2.
|
|
117
|
+
*/
|
|
118
|
+
const DEFAULT_FONT = {
|
|
119
|
+
fallbackFontFamily: "sans-serif",
|
|
120
|
+
fontFamily: "Inter",
|
|
121
|
+
webFont: {
|
|
122
|
+
url: "https://cdn.usemaily.com/fonts/v0/inter.woff2",
|
|
123
|
+
format: "woff2"
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Injects a @font-face style element into the document head to load
|
|
128
|
+
* the given font. Only works in browser environments.
|
|
129
|
+
*/
|
|
130
|
+
function loadFont(font) {
|
|
131
|
+
const style = getFontFaceStyle(font);
|
|
132
|
+
const styleElement = document.createElement("style");
|
|
133
|
+
styleElement.textContent = style;
|
|
134
|
+
document.head.appendChild(styleElement);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Generates a @font-face CSS rule string for the given font configuration.
|
|
138
|
+
* Includes the font family, style, weight, MSO fallback, and web font
|
|
139
|
+
* source URL when available.
|
|
140
|
+
*/
|
|
141
|
+
function getFontFaceStyle(font) {
|
|
142
|
+
const { fontFamily, fallbackFontFamily, webFont, fontWeight = 400, fontStyle = "normal" } = font;
|
|
143
|
+
return `@font-face {font-family: '${fontFamily}';font-style: ${fontStyle};font-weight: ${fontWeight};mso-font-alt: '${fallbackFontFamily}';${webFont ? `src: url(${webFont.url}) format('${webFont.format}');` : ""}}`;
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/css-variables.ts
|
|
147
|
+
/**
|
|
148
|
+
* Returns a CSS variable object for the given name and value.
|
|
149
|
+
* If the value is nullish or empty, returns an empty object so that
|
|
150
|
+
* the default CSS variable value is not overridden. Numbers are
|
|
151
|
+
* automatically suffixed with "px".
|
|
152
|
+
*/
|
|
153
|
+
function getVariableValue(name, value) {
|
|
154
|
+
if (value === void 0 || value === null || value === "") return {};
|
|
155
|
+
return { [name]: typeof value === "number" ? `${value}px` : value };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Converts an EditorThemeOptions object into a flat map of Maily CSS
|
|
159
|
+
* custom properties (--mly-*). Used to apply theme overrides as inline
|
|
160
|
+
* styles on the editor or renderer root element. Covers body background,
|
|
161
|
+
* container dimensions and borders, button colors, link color, and
|
|
162
|
+
* font family settings.
|
|
163
|
+
*/
|
|
164
|
+
function getMailyCssVariables(theme) {
|
|
165
|
+
const font = theme.font || DEFAULT_FONT;
|
|
166
|
+
return {
|
|
167
|
+
...getVariableValue("--mly-body-background-color", theme.body?.backgroundColor),
|
|
168
|
+
...getVariableValue("--mly-body-padding-top", theme.body?.paddingTop),
|
|
169
|
+
...getVariableValue("--mly-body-padding-right", theme.body?.paddingRight),
|
|
170
|
+
...getVariableValue("--mly-body-padding-bottom", theme.body?.paddingBottom),
|
|
171
|
+
...getVariableValue("--mly-body-padding-left", theme.body?.paddingLeft),
|
|
172
|
+
...getVariableValue("--mly-container-background-color", theme.container?.backgroundColor),
|
|
173
|
+
...getVariableValue("--mly-container-max-width", theme.container?.maxWidth),
|
|
174
|
+
...getVariableValue("--mly-container-min-width", theme.container?.minWidth),
|
|
175
|
+
...getVariableValue("--mly-container-padding-top", theme.container?.paddingTop),
|
|
176
|
+
...getVariableValue("--mly-container-padding-right", theme.container?.paddingRight),
|
|
177
|
+
...getVariableValue("--mly-container-padding-bottom", theme.container?.paddingBottom),
|
|
178
|
+
...getVariableValue("--mly-container-padding-left", theme.container?.paddingLeft),
|
|
179
|
+
...getVariableValue("--mly-container-border-radius", theme.container?.borderRadius),
|
|
180
|
+
...getVariableValue("--mly-container-border-width", theme.container?.borderWidth),
|
|
181
|
+
...getVariableValue("--mly-container-border-color", theme.container?.borderColor),
|
|
182
|
+
...getVariableValue("--mly-button-background-color", theme.button?.backgroundColor),
|
|
183
|
+
...getVariableValue("--mly-button-text-color", theme.button?.color),
|
|
184
|
+
...getVariableValue("--mly-button-padding-top", theme.button?.paddingTop),
|
|
185
|
+
...getVariableValue("--mly-button-padding-right", theme.button?.paddingRight),
|
|
186
|
+
...getVariableValue("--mly-button-padding-bottom", theme.button?.paddingBottom),
|
|
187
|
+
...getVariableValue("--mly-button-padding-left", theme.button?.paddingLeft),
|
|
188
|
+
...getVariableValue("--mly-link-color", theme.link?.color),
|
|
189
|
+
...getVariableValue("--mly-font-family", font.fontFamily),
|
|
190
|
+
...getVariableValue("--mly-font-fallback-family", font.fallbackFontFamily),
|
|
191
|
+
"--mly-font": `var(--mly-font-family), var(--mly-font-fallback-family)`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/html-code-block.ts
|
|
196
|
+
/**
|
|
197
|
+
* Tab options for the HTML code block node.
|
|
198
|
+
* "code" shows the raw HTML source, "preview" renders it visually.
|
|
199
|
+
*/
|
|
200
|
+
const HTML_CODE_BLOCK_TABS = {
|
|
201
|
+
CODE: "code",
|
|
202
|
+
PREVIEW: "preview"
|
|
203
|
+
};
|
|
204
|
+
const DEFAULT_HTML_CODE_BLOCK_TAB = HTML_CODE_BLOCK_TABS.CODE;
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/math.ts
|
|
207
|
+
function clamp(value, [min, max]) {
|
|
208
|
+
return Math.min(Math.max(value, min), max);
|
|
209
|
+
}
|
|
210
|
+
function percentage(portion, total) {
|
|
211
|
+
return clamp(portion / total * 100, [0, 100]);
|
|
212
|
+
}
|
|
213
|
+
function absoluteFromPercentage(percentage, total) {
|
|
214
|
+
return Math.round(total * percentage / 100);
|
|
215
|
+
}
|
|
216
|
+
function roundTo(value, decimals) {
|
|
217
|
+
const factor = Math.pow(10, decimals);
|
|
218
|
+
return Math.round(value * factor) / factor;
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/image-width.ts
|
|
222
|
+
const MIN_IMAGE_WIDTH_PERCENTAGE = 5;
|
|
223
|
+
const MAX_IMAGE_WIDTH_PERCENTAGE = 100;
|
|
224
|
+
const MAX_IMAGE_WIDTH_PIXELS = 600;
|
|
225
|
+
/**
|
|
226
|
+
* Converts any ImageWidth value to a clamped percentage number.
|
|
227
|
+
* Handles "auto" (→ 100), percentage strings (e.g. "50%" → 50),
|
|
228
|
+
* and legacy pixel numbers (converted relative to maxWidthPixels).
|
|
229
|
+
* Returns 100 for null or unrecognized values.
|
|
230
|
+
*/
|
|
231
|
+
function parseWidthToPercentage(width, maxWidthPixels = MAX_IMAGE_WIDTH_PIXELS) {
|
|
232
|
+
if (typeof width === "string") {
|
|
233
|
+
if (width === "auto") return 100;
|
|
234
|
+
return width.endsWith("%") ? clampImageWidthPercentage(parseInt(width)) : 100;
|
|
235
|
+
}
|
|
236
|
+
if (typeof width === "number") return clampImageWidthPercentage(Math.round(100 * width / maxWidthPixels));
|
|
237
|
+
return 100;
|
|
238
|
+
}
|
|
239
|
+
function clampImageWidthPercentage(width) {
|
|
240
|
+
return clamp(width, [5, 100]);
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/mark.ts
|
|
244
|
+
/** Registry of all inline mark type strings used in the document JSON. */
|
|
245
|
+
const MAILY_MARK_TYPES = {
|
|
246
|
+
BOLD: "bold",
|
|
247
|
+
ITALIC: "italic",
|
|
248
|
+
STRIKE: "strike",
|
|
249
|
+
UNDERLINE: "underline",
|
|
250
|
+
LINK: "link",
|
|
251
|
+
CODE: "code",
|
|
252
|
+
TEXT_STYLE: "textStyle",
|
|
253
|
+
HIGHLIGHT: "highlight"
|
|
254
|
+
};
|
|
255
|
+
const allowedMarkTypes = Object.values(MAILY_MARK_TYPES);
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/node.ts
|
|
258
|
+
/**
|
|
259
|
+
* Registry of all document node type strings. These map 1:1 to
|
|
260
|
+
* Tiptap extension names and are used as discriminators in the
|
|
261
|
+
* document JSON.
|
|
262
|
+
*/
|
|
263
|
+
const MAILY_NODE_TYPES = {
|
|
264
|
+
DOCUMENT: "doc",
|
|
265
|
+
PARAGRAPH: "paragraph",
|
|
266
|
+
TEXT: "text",
|
|
267
|
+
HEADING: "heading",
|
|
268
|
+
VARIABLE: "variable",
|
|
269
|
+
IMAGE: "image",
|
|
270
|
+
SPACER: "spacer",
|
|
271
|
+
SECTION: "section",
|
|
272
|
+
COLUMNS: "columns",
|
|
273
|
+
COLUMN: "column",
|
|
274
|
+
BULLET_LIST: "bulletList",
|
|
275
|
+
ORDERED_LIST: "orderedList",
|
|
276
|
+
LIST_ITEM: "listItem",
|
|
277
|
+
HORIZONTAL_RULE: "horizontalRule",
|
|
278
|
+
BUTTON: "button",
|
|
279
|
+
REPEAT: "repeat",
|
|
280
|
+
HTML_CODE_BLOCK: "htmlCodeBlock",
|
|
281
|
+
BLOCKQUOTE: "blockquote",
|
|
282
|
+
HARD_BREAK: "hardBreak",
|
|
283
|
+
FOOTER: "footer",
|
|
284
|
+
INLINE_IMAGE: "inlineImage",
|
|
285
|
+
LINK_CARD: "linkCard"
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Tiptap extension types that attach attributes to nodes but are
|
|
289
|
+
* not document nodes themselves (e.g. fontStyle, visibility).
|
|
290
|
+
*/
|
|
291
|
+
const MAILY_EXTENSION_TYPES = {
|
|
292
|
+
FONT_STYLE: "fontStyle",
|
|
293
|
+
VISIBILITY: "visibility"
|
|
294
|
+
};
|
|
295
|
+
const allowedNodeTypes = Object.values(MAILY_NODE_TYPES);
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/is.ts
|
|
298
|
+
function guard(type) {
|
|
299
|
+
return (node) => node.type === type;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Type guards for Maily nodes and marks. Provides narrowing functions
|
|
303
|
+
* for every node type (is.paragraph, is.button, etc.) and mark type
|
|
304
|
+
* (is.bold, is.link, etc.), plus structural checks like is.parent
|
|
305
|
+
* and is.marked. Each guard narrows the input to its specific type.
|
|
306
|
+
*
|
|
307
|
+
* Example:
|
|
308
|
+
* if (is.paragraph(node)) {
|
|
309
|
+
* node.attrs.textAlign // narrowed to ParagraphNode
|
|
310
|
+
* }
|
|
311
|
+
*/
|
|
312
|
+
const is = {
|
|
313
|
+
any(value) {
|
|
314
|
+
return this.node(value) || this.mark(value);
|
|
315
|
+
},
|
|
316
|
+
parent(node) {
|
|
317
|
+
return Array.isArray(node?.content);
|
|
318
|
+
},
|
|
319
|
+
marked(node) {
|
|
320
|
+
return Array.isArray(node?.marks);
|
|
321
|
+
},
|
|
322
|
+
node(node) {
|
|
323
|
+
return typeof node === "object" && node !== null && "type" in node && allowedNodeTypes.includes(node.type);
|
|
324
|
+
},
|
|
325
|
+
doc: guard(MAILY_NODE_TYPES.DOCUMENT),
|
|
326
|
+
paragraph: guard(MAILY_NODE_TYPES.PARAGRAPH),
|
|
327
|
+
text: guard(MAILY_NODE_TYPES.TEXT),
|
|
328
|
+
heading: guard(MAILY_NODE_TYPES.HEADING),
|
|
329
|
+
variable: guard(MAILY_NODE_TYPES.VARIABLE),
|
|
330
|
+
image: guard(MAILY_NODE_TYPES.IMAGE),
|
|
331
|
+
inlineImage: guard(MAILY_NODE_TYPES.INLINE_IMAGE),
|
|
332
|
+
spacer: guard(MAILY_NODE_TYPES.SPACER),
|
|
333
|
+
section: guard(MAILY_NODE_TYPES.SECTION),
|
|
334
|
+
button: guard(MAILY_NODE_TYPES.BUTTON),
|
|
335
|
+
columns: guard(MAILY_NODE_TYPES.COLUMNS),
|
|
336
|
+
column: guard(MAILY_NODE_TYPES.COLUMN),
|
|
337
|
+
repeat: guard(MAILY_NODE_TYPES.REPEAT),
|
|
338
|
+
bulletList: guard(MAILY_NODE_TYPES.BULLET_LIST),
|
|
339
|
+
orderedList: guard(MAILY_NODE_TYPES.ORDERED_LIST),
|
|
340
|
+
listItem: guard(MAILY_NODE_TYPES.LIST_ITEM),
|
|
341
|
+
horizontalRule: guard(MAILY_NODE_TYPES.HORIZONTAL_RULE),
|
|
342
|
+
htmlCodeBlock: guard(MAILY_NODE_TYPES.HTML_CODE_BLOCK),
|
|
343
|
+
blockquote: guard(MAILY_NODE_TYPES.BLOCKQUOTE),
|
|
344
|
+
hardBreak: guard(MAILY_NODE_TYPES.HARD_BREAK),
|
|
345
|
+
footer: guard(MAILY_NODE_TYPES.FOOTER),
|
|
346
|
+
linkCard: guard(MAILY_NODE_TYPES.LINK_CARD),
|
|
347
|
+
mark(mark) {
|
|
348
|
+
return typeof mark === "object" && mark !== null && "type" in mark && allowedMarkTypes.includes(mark.type);
|
|
349
|
+
},
|
|
350
|
+
bold: guard(MAILY_MARK_TYPES.BOLD),
|
|
351
|
+
italic: guard(MAILY_MARK_TYPES.ITALIC),
|
|
352
|
+
strike: guard(MAILY_MARK_TYPES.STRIKE),
|
|
353
|
+
underline: guard(MAILY_MARK_TYPES.UNDERLINE),
|
|
354
|
+
link: guard(MAILY_MARK_TYPES.LINK),
|
|
355
|
+
code: guard(MAILY_MARK_TYPES.CODE),
|
|
356
|
+
textStyle: guard(MAILY_MARK_TYPES.TEXT_STYLE),
|
|
357
|
+
highlight: guard(MAILY_MARK_TYPES.HIGHLIGHT)
|
|
358
|
+
};
|
|
359
|
+
/** Type guard that checks if a value is defined (not null or undefined). */
|
|
360
|
+
function isDef(val) {
|
|
361
|
+
return val !== void 0 && val !== null;
|
|
362
|
+
}
|
|
363
|
+
/** Type guard that checks if a value is a boolean. */
|
|
364
|
+
function isBoolean(val) {
|
|
365
|
+
return typeof val === "boolean";
|
|
366
|
+
}
|
|
367
|
+
/** Type guard that checks if a value is a number. */
|
|
368
|
+
function isNumber(val) {
|
|
369
|
+
return typeof val === "number";
|
|
370
|
+
}
|
|
371
|
+
/** Type guard that checks if a value is a string. */
|
|
372
|
+
function isString(val) {
|
|
373
|
+
return typeof val === "string";
|
|
374
|
+
}
|
|
375
|
+
/** Type guard that checks if a value is a non-null object. */
|
|
376
|
+
function isObject(val) {
|
|
377
|
+
return typeof val === "object" && val !== null;
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/font-style.ts
|
|
381
|
+
/**
|
|
382
|
+
* Built-in font options served from the Maily CDN. System fonts
|
|
383
|
+
* (Arial, Helvetica, etc.) are included without a webFont field.
|
|
384
|
+
*/
|
|
385
|
+
const DEFAULT_FONT_FAMILIES = [
|
|
386
|
+
{
|
|
387
|
+
fontFamily: "Inter",
|
|
388
|
+
webFont: {
|
|
389
|
+
url: "https://cdn.usemaily.com/fonts/v0/inter.woff2",
|
|
390
|
+
format: "woff2"
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
fontFamily: "Roboto",
|
|
395
|
+
webFont: {
|
|
396
|
+
url: "https://cdn.usemaily.com/fonts/v0/roboto.woff2",
|
|
397
|
+
format: "woff2"
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
fontFamily: "Open Sans",
|
|
402
|
+
webFont: {
|
|
403
|
+
url: "https://cdn.usemaily.com/fonts/v0/open-sans.woff2",
|
|
404
|
+
format: "woff2"
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
fontFamily: "Lato",
|
|
409
|
+
webFont: {
|
|
410
|
+
url: "https://cdn.usemaily.com/fonts/v0/lato.woff2",
|
|
411
|
+
format: "woff2"
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
fontFamily: "Montserrat",
|
|
416
|
+
webFont: {
|
|
417
|
+
url: "https://cdn.usemaily.com/fonts/v0/montserrat.woff2",
|
|
418
|
+
format: "woff2"
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
fontFamily: "Poppins",
|
|
423
|
+
webFont: {
|
|
424
|
+
url: "https://cdn.usemaily.com/fonts/v0/poppins.woff2",
|
|
425
|
+
format: "woff2"
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
fontFamily: "Raleway",
|
|
430
|
+
webFont: {
|
|
431
|
+
url: "https://cdn.usemaily.com/fonts/v0/raleway.woff2",
|
|
432
|
+
format: "woff2"
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
fontFamily: "Ubuntu",
|
|
437
|
+
webFont: {
|
|
438
|
+
url: "https://cdn.usemaily.com/fonts/v0/ubuntu.woff2",
|
|
439
|
+
format: "woff2"
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
{ fontFamily: "Arial" },
|
|
443
|
+
{ fontFamily: "Helvetica" },
|
|
444
|
+
{ fontFamily: "Georgia" },
|
|
445
|
+
{ fontFamily: "Times New Roman" },
|
|
446
|
+
{ fontFamily: "Verdana" },
|
|
447
|
+
{ fontFamily: "Courier New" },
|
|
448
|
+
{ fontFamily: "Trebuchet MS" }
|
|
449
|
+
];
|
|
450
|
+
const FONT_STYLES = {
|
|
451
|
+
NORMAL: "normal",
|
|
452
|
+
ITALIC: "italic"
|
|
453
|
+
};
|
|
454
|
+
/** Preset options for the font size dropdown in the editor. */
|
|
455
|
+
const FONT_SIZE_PRESETS = [
|
|
456
|
+
{
|
|
457
|
+
value: 12,
|
|
458
|
+
label: "Small"
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
value: 14,
|
|
462
|
+
label: "Normal"
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
value: 16,
|
|
466
|
+
label: "Medium"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
value: 18,
|
|
470
|
+
label: "Large"
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
value: 24,
|
|
474
|
+
label: "XL"
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
value: 32,
|
|
478
|
+
label: "2XL"
|
|
479
|
+
}
|
|
480
|
+
];
|
|
481
|
+
/** Preset options for the line height dropdown in the editor. */
|
|
482
|
+
const LINE_HEIGHT_PRESETS = [
|
|
483
|
+
{
|
|
484
|
+
value: 1,
|
|
485
|
+
label: "Tight"
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
value: 1.25,
|
|
489
|
+
label: "Snug"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
value: 1.5,
|
|
493
|
+
label: "Normal"
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
value: 1.75,
|
|
497
|
+
label: "Relaxed"
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
value: 2,
|
|
501
|
+
label: "Loose"
|
|
502
|
+
}
|
|
503
|
+
];
|
|
504
|
+
const DEFAULT_FONT_FAMILY = "Inter";
|
|
505
|
+
const DEFAULT_FONT_SIZE = 15;
|
|
506
|
+
const DEFAULT_FONT_WEIGHT = 400;
|
|
507
|
+
const DEFAULT_LINE_HEIGHT = 1.75;
|
|
508
|
+
const DEFAULT_FONT_FALLBACK = "sans-serif";
|
|
509
|
+
/**
|
|
510
|
+
* Builds a CSS properties object from a node's font attributes,
|
|
511
|
+
* falling back to the provided defaults for any null values.
|
|
512
|
+
* Only includes properties that have a resolved value.
|
|
513
|
+
*/
|
|
514
|
+
function getFontStyle(attrs, defaults) {
|
|
515
|
+
const { fontFamily, fontFallback, fontSize, fontWeight, lineHeight, fontStyle } = attrs;
|
|
516
|
+
const size = fontSize ?? defaults?.fontSize;
|
|
517
|
+
const weight = fontWeight ?? defaults?.fontWeight;
|
|
518
|
+
const line = lineHeight ?? defaults?.lineHeight;
|
|
519
|
+
const style = fontStyle ?? defaults?.fontStyle;
|
|
520
|
+
const fontFamilyParts = [fontFamily, fontFallback].filter(Boolean);
|
|
521
|
+
return {
|
|
522
|
+
...fontFamilyParts.length > 0 && { fontFamily: fontFamilyParts.join(", ") },
|
|
523
|
+
...isDef(size) ? { fontSize: `${size}px` } : {},
|
|
524
|
+
...isDef(weight) ? { fontWeight: weight } : {},
|
|
525
|
+
...isDef(line) ? { lineHeight: line } : {},
|
|
526
|
+
...isDef(style) ? { fontStyle: style } : {}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/font-weight.ts
|
|
531
|
+
/**
|
|
532
|
+
* Numeric font weight values mapped to semantic names.
|
|
533
|
+
* Normal (400), Medium (500), Semibold (600), Bold (700), Extra Bold (800).
|
|
534
|
+
*/
|
|
535
|
+
const FONT_WEIGHTS = {
|
|
536
|
+
NORMAL: 400,
|
|
537
|
+
MEDIUM: 500,
|
|
538
|
+
SEMIBOLD: 600,
|
|
539
|
+
BOLD: 700,
|
|
540
|
+
EXTRA_BOLD: 800
|
|
541
|
+
};
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/function.ts
|
|
544
|
+
/** A no-operation function. Useful as a default callback or placeholder. */
|
|
545
|
+
function noop() {}
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/invariant.ts
|
|
548
|
+
function invariant(value, message) {
|
|
549
|
+
if (value === false || value === null || typeof value === "undefined") {
|
|
550
|
+
console.error(`${MAILY_ERROR_PREFIX}: The following error is a bug in Maily;
|
|
551
|
+
Please open an issue! ${MAILY_BUG_REPORT_URL}`);
|
|
552
|
+
throw new Error(message);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
//#endregion
|
|
556
|
+
//#region src/margin.ts
|
|
557
|
+
/**
|
|
558
|
+
* Converts a MarginStyleConfig into CSS properties for inline styles.
|
|
559
|
+
* When all sides are equal (or mode is "uniform"), emits shorthand CSS;
|
|
560
|
+
* otherwise emits per-side longhand properties.
|
|
561
|
+
*/
|
|
562
|
+
function getMarginStyle(config) {
|
|
563
|
+
const { marginMode, marginTop, marginRight, marginBottom, marginLeft } = config;
|
|
564
|
+
if (marginMode === FIELD_MODE.UNIFORM || marginLeft === marginRight && marginRight === marginBottom && marginBottom === marginTop) return { margin: `${marginTop}px` };
|
|
565
|
+
return {
|
|
566
|
+
marginTop: `${marginTop}px`,
|
|
567
|
+
marginRight: `${marginRight}px`,
|
|
568
|
+
marginBottom: `${marginBottom}px`,
|
|
569
|
+
marginLeft: `${marginLeft}px`
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
//#endregion
|
|
573
|
+
//#region src/object.ts
|
|
574
|
+
function isMergableObject(value) {
|
|
575
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* One-level deep merge of plain objects. Nested plain objects are
|
|
579
|
+
* shallow-merged; primitives, arrays, and null are replaced outright.
|
|
580
|
+
* Does not mutate the target — returns a new object.
|
|
581
|
+
*
|
|
582
|
+
* Example:
|
|
583
|
+
* deepMerge({ a: 1, nested: { x: 1, y: 2 } }, { nested: { y: 3 } })
|
|
584
|
+
* // => { a: 1, nested: { x: 1, y: 3 } }
|
|
585
|
+
*/
|
|
586
|
+
function deepMerge(target, ...sources) {
|
|
587
|
+
const result = { ...target };
|
|
588
|
+
for (const source of sources) for (const [key, value] of Object.entries(source)) result[key] = isMergableObject(value) && isMergableObject(result[key]) ? {
|
|
589
|
+
...result[key],
|
|
590
|
+
...value
|
|
591
|
+
} : value;
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
function isObjectDeepEqual(first, second) {
|
|
595
|
+
if (first === second) return true;
|
|
596
|
+
for (const key of Object.keys(first)) if (first[key] !== second[key]) return false;
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/padding.ts
|
|
601
|
+
/**
|
|
602
|
+
* Converts a PaddingStyleConfig into CSS properties for inline styles.
|
|
603
|
+
* When all sides are equal (or mode is "uniform"), emits shorthand CSS;
|
|
604
|
+
* otherwise emits per-side longhand properties.
|
|
605
|
+
*/
|
|
606
|
+
function getPaddingStyle(config) {
|
|
607
|
+
const { paddingMode, paddingTop, paddingRight, paddingBottom, paddingLeft } = config;
|
|
608
|
+
if (paddingMode === FIELD_MODE.UNIFORM || paddingLeft === paddingRight && paddingRight === paddingBottom && paddingBottom === paddingTop) return { padding: `${paddingTop}px` };
|
|
609
|
+
return {
|
|
610
|
+
paddingTop: `${paddingTop}px`,
|
|
611
|
+
paddingRight: `${paddingRight}px`,
|
|
612
|
+
paddingBottom: `${paddingBottom}px`,
|
|
613
|
+
paddingLeft: `${paddingLeft}px`
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/promise.ts
|
|
618
|
+
/** Returns a promise that resolves after the given number of milliseconds. */
|
|
619
|
+
function sleep(ms) {
|
|
620
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
621
|
+
}
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/text-direction.ts
|
|
624
|
+
/** Text direction values matching the HTML `dir` attribute: ltr, rtl, auto. */
|
|
625
|
+
const TEXT_DIRECTIONS = {
|
|
626
|
+
LTR: "ltr",
|
|
627
|
+
RTL: "rtl",
|
|
628
|
+
AUTO: "auto"
|
|
629
|
+
};
|
|
630
|
+
const allowedTextDirections = Object.freeze(Object.values(TEXT_DIRECTIONS));
|
|
631
|
+
const DEFAULT_TEXT_DIRECTION = TEXT_DIRECTIONS.AUTO;
|
|
632
|
+
function isAllowedTextDirection(value) {
|
|
633
|
+
return typeof value === "string" && allowedTextDirections.includes(value);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Returns a CSS `direction` property object for the given dir value.
|
|
637
|
+
* Returns an empty object for "auto" or nullish values, since the
|
|
638
|
+
* browser default is sufficient in those cases.
|
|
639
|
+
*/
|
|
640
|
+
function getCssDirection(dir) {
|
|
641
|
+
if (dir && dir !== TEXT_DIRECTIONS.AUTO) return { direction: dir };
|
|
642
|
+
return {};
|
|
643
|
+
}
|
|
644
|
+
//#endregion
|
|
645
|
+
//#region src/theme.ts
|
|
646
|
+
const DEFAULT_LINK_TEXT_COLOR = "#111827";
|
|
647
|
+
const DEFAULT_RENDERER_THEME = {
|
|
648
|
+
paragraph: {
|
|
649
|
+
color: "#374151",
|
|
650
|
+
fontSize: 15,
|
|
651
|
+
lineHeight: 1.75,
|
|
652
|
+
fontWeight: FONT_WEIGHTS.NORMAL
|
|
653
|
+
},
|
|
654
|
+
heading: {
|
|
655
|
+
defaults: { color: "#111827" },
|
|
656
|
+
h1: {
|
|
657
|
+
fontSize: 36,
|
|
658
|
+
lineHeight: 1.1111111,
|
|
659
|
+
fontWeight: FONT_WEIGHTS.SEMIBOLD
|
|
660
|
+
},
|
|
661
|
+
h2: {
|
|
662
|
+
fontSize: 30,
|
|
663
|
+
lineHeight: 1.3333333,
|
|
664
|
+
fontWeight: FONT_WEIGHTS.SEMIBOLD
|
|
665
|
+
},
|
|
666
|
+
h3: {
|
|
667
|
+
fontSize: 24,
|
|
668
|
+
lineHeight: 1.6,
|
|
669
|
+
fontWeight: FONT_WEIGHTS.SEMIBOLD
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
footer: {
|
|
673
|
+
color: "#64748B",
|
|
674
|
+
fontSize: 14,
|
|
675
|
+
lineHeight: 1.7142857,
|
|
676
|
+
fontWeight: FONT_WEIGHTS.NORMAL
|
|
677
|
+
},
|
|
678
|
+
blockquote: {
|
|
679
|
+
color: "#374151",
|
|
680
|
+
borderColor: "#D1D5DB",
|
|
681
|
+
fontSize: 15,
|
|
682
|
+
lineHeight: 1.75,
|
|
683
|
+
fontWeight: FONT_WEIGHTS.MEDIUM,
|
|
684
|
+
fontStyle: FONT_STYLES.ITALIC
|
|
685
|
+
},
|
|
686
|
+
horizontalRule: { color: "#EAEAEA" },
|
|
687
|
+
code: {
|
|
688
|
+
color: "#111827",
|
|
689
|
+
backgroundColor: "#EFEFEF"
|
|
690
|
+
},
|
|
691
|
+
linkCard: {
|
|
692
|
+
titleColor: "#111827",
|
|
693
|
+
descriptionColor: "#374151",
|
|
694
|
+
badgeTextColor: "#374151",
|
|
695
|
+
badgeBackgroundColor: "#fff085",
|
|
696
|
+
subTitleColor: "#6B7280",
|
|
697
|
+
borderColor: "#E5E7EB"
|
|
698
|
+
},
|
|
699
|
+
container: {
|
|
700
|
+
backgroundColor: "#ffffff",
|
|
701
|
+
maxWidth: 600,
|
|
702
|
+
minWidth: 300,
|
|
703
|
+
paddingTop: 8,
|
|
704
|
+
paddingRight: 8,
|
|
705
|
+
paddingBottom: 8,
|
|
706
|
+
paddingLeft: 8,
|
|
707
|
+
borderRadius: 0,
|
|
708
|
+
borderWidth: 0,
|
|
709
|
+
borderColor: "transparent"
|
|
710
|
+
},
|
|
711
|
+
body: {
|
|
712
|
+
backgroundColor: "#ffffff",
|
|
713
|
+
paddingTop: 0,
|
|
714
|
+
paddingRight: 0,
|
|
715
|
+
paddingBottom: 0,
|
|
716
|
+
paddingLeft: 0
|
|
717
|
+
},
|
|
718
|
+
button: {
|
|
719
|
+
backgroundColor: "#000000",
|
|
720
|
+
color: "#ffffff",
|
|
721
|
+
paddingTop: 10,
|
|
722
|
+
paddingRight: 32,
|
|
723
|
+
paddingBottom: 10,
|
|
724
|
+
paddingLeft: 32,
|
|
725
|
+
fontSize: 14,
|
|
726
|
+
lineHeight: 1.4285714,
|
|
727
|
+
fontWeight: FONT_WEIGHTS.SEMIBOLD
|
|
728
|
+
},
|
|
729
|
+
link: { color: DEFAULT_LINK_TEXT_COLOR },
|
|
730
|
+
listMarker: { color: "#d1d5dc" },
|
|
731
|
+
font: DEFAULT_FONT
|
|
732
|
+
};
|
|
733
|
+
const DEFAULT_EDITOR_THEME = {
|
|
734
|
+
container: {
|
|
735
|
+
backgroundColor: "#ffffff",
|
|
736
|
+
maxWidth: 600,
|
|
737
|
+
minWidth: 300,
|
|
738
|
+
paddingTop: 8,
|
|
739
|
+
paddingRight: 8,
|
|
740
|
+
paddingBottom: 8,
|
|
741
|
+
paddingLeft: 8,
|
|
742
|
+
borderRadius: 0,
|
|
743
|
+
borderWidth: 0,
|
|
744
|
+
borderColor: "transparent"
|
|
745
|
+
},
|
|
746
|
+
body: {
|
|
747
|
+
backgroundColor: "#ffffff",
|
|
748
|
+
paddingTop: 0,
|
|
749
|
+
paddingRight: 0,
|
|
750
|
+
paddingBottom: 0,
|
|
751
|
+
paddingLeft: 0
|
|
752
|
+
},
|
|
753
|
+
button: {
|
|
754
|
+
backgroundColor: "#000000",
|
|
755
|
+
color: "#ffffff",
|
|
756
|
+
paddingTop: 10,
|
|
757
|
+
paddingRight: 32,
|
|
758
|
+
paddingBottom: 10,
|
|
759
|
+
paddingLeft: 32
|
|
760
|
+
},
|
|
761
|
+
link: { color: DEFAULT_LINK_TEXT_COLOR },
|
|
762
|
+
font: DEFAULT_FONT
|
|
763
|
+
};
|
|
764
|
+
/**
|
|
765
|
+
* Resolves the baseline font defaults for a node type from the
|
|
766
|
+
* renderer theme. Falls back to paragraph defaults for unknown types.
|
|
767
|
+
*/
|
|
768
|
+
function getNodeFontStyleDefaults(nodeType, level) {
|
|
769
|
+
const paragraph = DEFAULT_RENDERER_THEME.paragraph;
|
|
770
|
+
const fallback = {
|
|
771
|
+
fontSize: paragraph.fontSize,
|
|
772
|
+
lineHeight: paragraph.lineHeight,
|
|
773
|
+
fontWeight: paragraph.fontWeight
|
|
774
|
+
};
|
|
775
|
+
if (nodeType === MAILY_NODE_TYPES.HEADING) {
|
|
776
|
+
const heading = DEFAULT_RENDERER_THEME.heading;
|
|
777
|
+
const key = `h${level ?? 1}`;
|
|
778
|
+
const entry = heading?.[key];
|
|
779
|
+
return {
|
|
780
|
+
fontSize: entry?.fontSize ?? fallback.fontSize,
|
|
781
|
+
lineHeight: entry?.lineHeight ?? fallback.lineHeight,
|
|
782
|
+
fontWeight: entry?.fontWeight ?? fallback.fontWeight
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
if (nodeType === MAILY_NODE_TYPES.BUTTON) {
|
|
786
|
+
const btn = DEFAULT_RENDERER_THEME.button;
|
|
787
|
+
return {
|
|
788
|
+
fontSize: btn?.fontSize ?? fallback.fontSize,
|
|
789
|
+
lineHeight: btn?.lineHeight ?? fallback.lineHeight,
|
|
790
|
+
fontWeight: btn?.fontWeight ?? fallback.fontWeight
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (nodeType === MAILY_NODE_TYPES.BLOCKQUOTE) {
|
|
794
|
+
const bq = DEFAULT_RENDERER_THEME.blockquote;
|
|
795
|
+
return {
|
|
796
|
+
fontSize: bq?.fontSize ?? fallback.fontSize,
|
|
797
|
+
lineHeight: bq?.lineHeight ?? fallback.lineHeight,
|
|
798
|
+
fontWeight: bq?.fontWeight ?? fallback.fontWeight,
|
|
799
|
+
fontStyle: bq?.fontStyle ?? void 0
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
if (nodeType === MAILY_NODE_TYPES.FOOTER) {
|
|
803
|
+
const ft = DEFAULT_RENDERER_THEME.footer;
|
|
804
|
+
return {
|
|
805
|
+
fontSize: ft?.fontSize ?? fallback.fontSize,
|
|
806
|
+
lineHeight: ft?.lineHeight ?? fallback.lineHeight,
|
|
807
|
+
fontWeight: ft?.fontWeight ?? fallback.fontWeight
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
return fallback;
|
|
811
|
+
}
|
|
812
|
+
//#endregion
|
|
813
|
+
//#region src/variable.ts
|
|
814
|
+
/** Default opening delimiter for variable placeholders in text. */
|
|
815
|
+
const DEFAULT_VARIABLE_START_TRIGGER = "{{";
|
|
816
|
+
/** Default closing delimiter for variable placeholders in text. */
|
|
817
|
+
const DEFAULT_VARIABLE_END_TRIGGER = "}}";
|
|
818
|
+
/**
|
|
819
|
+
* Regex that matches variable placeholders like {{name}} in a string.
|
|
820
|
+
* Uses a non-greedy match to handle multiple variables in one string.
|
|
821
|
+
*/
|
|
822
|
+
const VARIABLE_PLACEHOLDER_REGEX = /({{.*?}})/g;
|
|
823
|
+
/**
|
|
824
|
+
* Delimiter used between fields inside a serialized variable string.
|
|
825
|
+
* Format: {{id|label|required|hideDefaultValue}}.
|
|
826
|
+
*/
|
|
827
|
+
const VARIABLE_TEXT_DELIMITER = "|";
|
|
828
|
+
/**
|
|
829
|
+
* Serializes a variable node's attributes into a delimited string.
|
|
830
|
+
* Output format: `{{id|label|required|hideDefaultValue}}`.
|
|
831
|
+
* Used to represent structured variable data as a template string
|
|
832
|
+
* in plain text contexts.
|
|
833
|
+
*/
|
|
834
|
+
function serializeVariableToText(variable) {
|
|
835
|
+
const { id, label, required = true, hideDefaultValue = false } = variable;
|
|
836
|
+
return [
|
|
837
|
+
"{{",
|
|
838
|
+
defaultStringifyValue(id),
|
|
839
|
+
"|",
|
|
840
|
+
defaultStringifyValue(label),
|
|
841
|
+
"|",
|
|
842
|
+
required,
|
|
843
|
+
"|",
|
|
844
|
+
hideDefaultValue,
|
|
845
|
+
"}}"
|
|
846
|
+
].join("");
|
|
847
|
+
}
|
|
848
|
+
function defaultStringifyValue(value) {
|
|
849
|
+
return value ?? "";
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Deserializes a variable string back into VariableAttributes.
|
|
853
|
+
* Expected input format: `{{id|label|required|hideDefaultValue}}`.
|
|
854
|
+
* Missing fields default to undefined; boolean fields are parsed
|
|
855
|
+
* from "true"/"false" strings.
|
|
856
|
+
*/
|
|
857
|
+
function deserializeVariableFromText(text) {
|
|
858
|
+
const [id, label, required, hideDefaultValue] = text.slice(2, text.length - 2).trim().split("|");
|
|
859
|
+
return {
|
|
860
|
+
id,
|
|
861
|
+
label: label || null,
|
|
862
|
+
required: parseBoolean(required, true),
|
|
863
|
+
hideDefaultValue: parseBoolean(hideDefaultValue, false)
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Parses a string containing variable placeholders (e.g. "Hello {{name}}")
|
|
868
|
+
* into a Tiptap-compatible document JSON with text and variable nodes.
|
|
869
|
+
* Splits on {{...}} boundaries and creates the appropriate node type
|
|
870
|
+
* for each segment.
|
|
871
|
+
*/
|
|
872
|
+
function buildDocFromVariableText(content) {
|
|
873
|
+
const parts = content.split(VARIABLE_PLACEHOLDER_REGEX);
|
|
874
|
+
return {
|
|
875
|
+
type: MAILY_NODE_TYPES.DOCUMENT,
|
|
876
|
+
content: [{
|
|
877
|
+
type: MAILY_NODE_TYPES.PARAGRAPH,
|
|
878
|
+
content: parts.flatMap((part) => {
|
|
879
|
+
if (part.startsWith("{{") && part.endsWith("}}")) return {
|
|
880
|
+
type: MAILY_NODE_TYPES.VARIABLE,
|
|
881
|
+
attrs: deserializeVariableFromText(part)
|
|
882
|
+
};
|
|
883
|
+
if (part) return {
|
|
884
|
+
type: MAILY_NODE_TYPES.TEXT,
|
|
885
|
+
text: part
|
|
886
|
+
};
|
|
887
|
+
return [];
|
|
888
|
+
}).filter(Boolean)
|
|
889
|
+
}]
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Checks if a string contains at least one variable placeholder.
|
|
894
|
+
* Returns true for strings like "Hello {{name}}", false for plain text.
|
|
895
|
+
* Uses index-based scanning instead of regex for performance.
|
|
896
|
+
*/
|
|
897
|
+
function hasVariableInText(text) {
|
|
898
|
+
const start = text.indexOf("{{");
|
|
899
|
+
if (start === -1) return false;
|
|
900
|
+
return text.indexOf("}}", start + 2) !== -1;
|
|
901
|
+
}
|
|
902
|
+
function parseBoolean(value, defaultValue) {
|
|
903
|
+
if (value === "true") return true;
|
|
904
|
+
if (value === "false") return false;
|
|
905
|
+
return defaultValue;
|
|
906
|
+
}
|
|
907
|
+
//#endregion
|
|
908
|
+
//#region src/visibility.ts
|
|
909
|
+
/**
|
|
910
|
+
* Comparison operators for conditional visibility rules.
|
|
911
|
+
* Used to evaluate a template variable against a value
|
|
912
|
+
* to decide whether a node should be shown or hidden.
|
|
913
|
+
*/
|
|
914
|
+
const VISIBILITY_OPERATORS = {
|
|
915
|
+
IS_TRUE: "is_true",
|
|
916
|
+
IS_FALSE: "is_false",
|
|
917
|
+
EQUALS: "equals",
|
|
918
|
+
NOT_EQUALS: "not_equals",
|
|
919
|
+
CONTAINS: "contains",
|
|
920
|
+
NOT_CONTAINS: "not_contains",
|
|
921
|
+
STARTS_WITH: "starts_with",
|
|
922
|
+
ENDS_WITH: "ends_with",
|
|
923
|
+
GREATER_THAN: "greater_than",
|
|
924
|
+
LESS_THAN: "less_than"
|
|
925
|
+
};
|
|
926
|
+
const VISIBILITY_OPERATOR_OPTIONS = [
|
|
927
|
+
{
|
|
928
|
+
value: VISIBILITY_OPERATORS.IS_TRUE,
|
|
929
|
+
label: "Is true"
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
value: VISIBILITY_OPERATORS.IS_FALSE,
|
|
933
|
+
label: "Is false"
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
value: VISIBILITY_OPERATORS.EQUALS,
|
|
937
|
+
label: "Equals"
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
value: VISIBILITY_OPERATORS.NOT_EQUALS,
|
|
941
|
+
label: "Not equals"
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
value: VISIBILITY_OPERATORS.CONTAINS,
|
|
945
|
+
label: "Contains"
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
value: VISIBILITY_OPERATORS.NOT_CONTAINS,
|
|
949
|
+
label: "Not contains"
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
value: VISIBILITY_OPERATORS.STARTS_WITH,
|
|
953
|
+
label: "Starts with"
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
value: VISIBILITY_OPERATORS.ENDS_WITH,
|
|
957
|
+
label: "Ends with"
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
value: VISIBILITY_OPERATORS.GREATER_THAN,
|
|
961
|
+
label: "Greater than"
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
value: VISIBILITY_OPERATORS.LESS_THAN,
|
|
965
|
+
label: "Less than"
|
|
966
|
+
}
|
|
967
|
+
];
|
|
968
|
+
/**
|
|
969
|
+
* Unary operators that don't require a comparison value.
|
|
970
|
+
* The UI hides the value input field when one of these is selected.
|
|
971
|
+
*/
|
|
972
|
+
const OPERATORS_WITHOUT_VALUE = [VISIBILITY_OPERATORS.IS_TRUE, VISIBILITY_OPERATORS.IS_FALSE];
|
|
973
|
+
const VISIBILITY_ACTIONS = {
|
|
974
|
+
NONE: "none",
|
|
975
|
+
SHOW: "show",
|
|
976
|
+
HIDE: "hide"
|
|
977
|
+
};
|
|
978
|
+
const VISIBILITY_ACTION_OPTIONS = [
|
|
979
|
+
{
|
|
980
|
+
value: VISIBILITY_ACTIONS.NONE,
|
|
981
|
+
label: "None"
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
value: VISIBILITY_ACTIONS.SHOW,
|
|
985
|
+
label: "Show if"
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
value: VISIBILITY_ACTIONS.HIDE,
|
|
989
|
+
label: "Hide if"
|
|
990
|
+
}
|
|
991
|
+
];
|
|
992
|
+
//#endregion
|
|
993
|
+
exports.BORDER_STYLES = BORDER_STYLES;
|
|
994
|
+
exports.BUTTON_KINDS = BUTTON_KINDS;
|
|
995
|
+
exports.DATA_NODE_TYPE_KEY = DATA_NODE_TYPE_KEY;
|
|
996
|
+
exports.DATA_VISIBILITY_RULE_KEY = DATA_VISIBILITY_RULE_KEY;
|
|
997
|
+
exports.DEFAULT_BORDER_COLOR = DEFAULT_BORDER_COLOR;
|
|
998
|
+
exports.DEFAULT_BORDER_STYLE = DEFAULT_BORDER_STYLE;
|
|
999
|
+
exports.DEFAULT_BUTTON_KIND = DEFAULT_BUTTON_KIND;
|
|
1000
|
+
exports.DEFAULT_EDITOR_THEME = DEFAULT_EDITOR_THEME;
|
|
1001
|
+
exports.DEFAULT_FONT = DEFAULT_FONT;
|
|
1002
|
+
exports.DEFAULT_FONT_FALLBACK = DEFAULT_FONT_FALLBACK;
|
|
1003
|
+
exports.DEFAULT_FONT_FAMILIES = DEFAULT_FONT_FAMILIES;
|
|
1004
|
+
exports.DEFAULT_FONT_FAMILY = DEFAULT_FONT_FAMILY;
|
|
1005
|
+
exports.DEFAULT_FONT_SIZE = DEFAULT_FONT_SIZE;
|
|
1006
|
+
exports.DEFAULT_FONT_WEIGHT = DEFAULT_FONT_WEIGHT;
|
|
1007
|
+
exports.DEFAULT_HTML_CODE_BLOCK_TAB = DEFAULT_HTML_CODE_BLOCK_TAB;
|
|
1008
|
+
exports.DEFAULT_LINE_HEIGHT = DEFAULT_LINE_HEIGHT;
|
|
1009
|
+
exports.DEFAULT_LINK_TEXT_COLOR = DEFAULT_LINK_TEXT_COLOR;
|
|
1010
|
+
exports.DEFAULT_RENDERER_THEME = DEFAULT_RENDERER_THEME;
|
|
1011
|
+
exports.DEFAULT_TEXT_ALIGN = DEFAULT_TEXT_ALIGN;
|
|
1012
|
+
exports.DEFAULT_TEXT_DIRECTION = DEFAULT_TEXT_DIRECTION;
|
|
1013
|
+
exports.DEFAULT_VARIABLE_END_TRIGGER = DEFAULT_VARIABLE_END_TRIGGER;
|
|
1014
|
+
exports.DEFAULT_VARIABLE_START_TRIGGER = DEFAULT_VARIABLE_START_TRIGGER;
|
|
1015
|
+
exports.FIELD_MODE = FIELD_MODE;
|
|
1016
|
+
exports.FONT_SIZE_PRESETS = FONT_SIZE_PRESETS;
|
|
1017
|
+
exports.FONT_STYLES = FONT_STYLES;
|
|
1018
|
+
exports.FONT_WEIGHTS = FONT_WEIGHTS;
|
|
1019
|
+
exports.HTML_CODE_BLOCK_TABS = HTML_CODE_BLOCK_TABS;
|
|
1020
|
+
exports.LINE_HEIGHT_PRESETS = LINE_HEIGHT_PRESETS;
|
|
1021
|
+
exports.MAILY_BUG_REPORT_URL = MAILY_BUG_REPORT_URL;
|
|
1022
|
+
exports.MAILY_ERROR_PREFIX = MAILY_ERROR_PREFIX;
|
|
1023
|
+
exports.MAILY_EXTENSION_TYPES = MAILY_EXTENSION_TYPES;
|
|
1024
|
+
exports.MAILY_MARK_TYPES = MAILY_MARK_TYPES;
|
|
1025
|
+
exports.MAILY_NODE_TYPES = MAILY_NODE_TYPES;
|
|
1026
|
+
exports.MAX_IMAGE_WIDTH_PERCENTAGE = MAX_IMAGE_WIDTH_PERCENTAGE;
|
|
1027
|
+
exports.MIN_IMAGE_WIDTH_PERCENTAGE = MIN_IMAGE_WIDTH_PERCENTAGE;
|
|
1028
|
+
exports.OPERATORS_WITHOUT_VALUE = OPERATORS_WITHOUT_VALUE;
|
|
1029
|
+
exports.TEXT_ALIGNMENTS = TEXT_ALIGNMENTS;
|
|
1030
|
+
exports.TEXT_DIRECTIONS = TEXT_DIRECTIONS;
|
|
1031
|
+
exports.VARIABLE_PLACEHOLDER_REGEX = VARIABLE_PLACEHOLDER_REGEX;
|
|
1032
|
+
exports.VARIABLE_TEXT_DELIMITER = VARIABLE_TEXT_DELIMITER;
|
|
1033
|
+
exports.VISIBILITY_ACTIONS = VISIBILITY_ACTIONS;
|
|
1034
|
+
exports.VISIBILITY_ACTION_OPTIONS = VISIBILITY_ACTION_OPTIONS;
|
|
1035
|
+
exports.VISIBILITY_OPERATORS = VISIBILITY_OPERATORS;
|
|
1036
|
+
exports.VISIBILITY_OPERATOR_OPTIONS = VISIBILITY_OPERATOR_OPTIONS;
|
|
1037
|
+
exports.absoluteFromPercentage = absoluteFromPercentage;
|
|
1038
|
+
exports.allowedButtonKinds = allowedButtonKinds;
|
|
1039
|
+
exports.allowedFallbackFonts = allowedFallbackFonts;
|
|
1040
|
+
exports.allowedFieldModes = allowedFieldModes;
|
|
1041
|
+
exports.allowedFontFormats = allowedFontFormats;
|
|
1042
|
+
exports.allowedMarkTypes = allowedMarkTypes;
|
|
1043
|
+
exports.allowedNodeTypes = allowedNodeTypes;
|
|
1044
|
+
exports.allowedTextAligns = allowedTextAligns;
|
|
1045
|
+
exports.allowedTextDirections = allowedTextDirections;
|
|
1046
|
+
exports.buildDocFromVariableText = buildDocFromVariableText;
|
|
1047
|
+
exports.clamp = clamp;
|
|
1048
|
+
exports.clampImageWidthPercentage = clampImageWidthPercentage;
|
|
1049
|
+
exports.deepMerge = deepMerge;
|
|
1050
|
+
exports.deserializeVariableFromText = deserializeVariableFromText;
|
|
1051
|
+
exports.getBorderStyle = getBorderStyle;
|
|
1052
|
+
exports.getCssDirection = getCssDirection;
|
|
1053
|
+
exports.getFontFaceStyle = getFontFaceStyle;
|
|
1054
|
+
exports.getFontStyle = getFontStyle;
|
|
1055
|
+
exports.getMailyCssVariables = getMailyCssVariables;
|
|
1056
|
+
exports.getMarginStyle = getMarginStyle;
|
|
1057
|
+
exports.getNodeFontStyleDefaults = getNodeFontStyleDefaults;
|
|
1058
|
+
exports.getPaddingStyle = getPaddingStyle;
|
|
1059
|
+
exports.getVariableValue = getVariableValue;
|
|
1060
|
+
exports.hasVariableInText = hasVariableInText;
|
|
1061
|
+
exports.invariant = invariant;
|
|
1062
|
+
exports.is = is;
|
|
1063
|
+
exports.isAllowedButtonKind = isAllowedButtonKind;
|
|
1064
|
+
exports.isAllowedFieldMode = isAllowedFieldMode;
|
|
1065
|
+
exports.isAllowedTextAlignment = isAllowedTextAlignment;
|
|
1066
|
+
exports.isAllowedTextDirection = isAllowedTextDirection;
|
|
1067
|
+
exports.isBoolean = isBoolean;
|
|
1068
|
+
exports.isDef = isDef;
|
|
1069
|
+
exports.isNumber = isNumber;
|
|
1070
|
+
exports.isObject = isObject;
|
|
1071
|
+
exports.isObjectDeepEqual = isObjectDeepEqual;
|
|
1072
|
+
exports.isString = isString;
|
|
1073
|
+
exports.loadFont = loadFont;
|
|
1074
|
+
exports.noop = noop;
|
|
1075
|
+
exports.parseWidthToPercentage = parseWidthToPercentage;
|
|
1076
|
+
exports.percentage = percentage;
|
|
1077
|
+
exports.roundTo = roundTo;
|
|
1078
|
+
exports.serializeVariableToText = serializeVariableToText;
|
|
1079
|
+
exports.sleep = sleep;
|
|
1080
|
+
|
|
1081
|
+
//# sourceMappingURL=index.cjs.map
|