@maily-to/migration 2.0.0-beta.6 → 2.0.0-beta.8
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/dist/index.cjs +434 -302
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +435 -303
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/readme.md +47 -8
package/dist/index.cjs
CHANGED
|
@@ -1,66 +1,68 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _maily_to_shared = require("@maily-to/shared");
|
|
3
|
-
//#region src/utils/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function splitBorderRadius(radius) {
|
|
12
|
-
return {
|
|
13
|
-
borderTopLeftRadius: radius,
|
|
14
|
-
borderTopRightRadius: radius,
|
|
15
|
-
borderBottomRightRadius: radius,
|
|
16
|
-
borderBottomLeftRadius: radius
|
|
17
|
-
};
|
|
3
|
+
//#region src/utils/style.ts
|
|
4
|
+
function getStyleAttribute(declarations) {
|
|
5
|
+
const rest = {};
|
|
6
|
+
for (const [property, value] of Object.entries(declarations)) {
|
|
7
|
+
if (value === null || value === void 0 || value === "") continue;
|
|
8
|
+
rest[property] = value;
|
|
9
|
+
}
|
|
10
|
+
return (0, _maily_to_shared.stringifyInlineStyles)(rest) || null;
|
|
18
11
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
function getPixelValue(value) {
|
|
13
|
+
return `${value}px`;
|
|
14
|
+
}
|
|
15
|
+
const BOX_SIDE_SUFFIXES = {
|
|
16
|
+
bottom: "Bottom",
|
|
17
|
+
left: "Left",
|
|
18
|
+
right: "Right",
|
|
19
|
+
top: "Top"
|
|
20
|
+
};
|
|
21
|
+
function getBoxDeclarations(options) {
|
|
22
|
+
const { bottom, left, property, right, top } = options;
|
|
23
|
+
const values = {
|
|
24
|
+
bottom,
|
|
25
|
+
left,
|
|
26
|
+
right,
|
|
27
|
+
top
|
|
32
28
|
};
|
|
29
|
+
const declarations = {};
|
|
30
|
+
for (const side of _maily_to_shared.BOX_SIDES) {
|
|
31
|
+
const value = values[side];
|
|
32
|
+
if (typeof value === "number") declarations[`${property}${BOX_SIDE_SUFFIXES[side]}`] = getPixelValue(value);
|
|
33
|
+
}
|
|
34
|
+
return declarations;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/utils/variable.ts
|
|
38
|
+
const LINK_PROTOCOL_REGEX = /https?:\/\//;
|
|
39
|
+
function getVariableReference(value) {
|
|
40
|
+
const name = value.replace(LINK_PROTOCOL_REGEX, "");
|
|
41
|
+
if (name.startsWith(_maily_to_shared.DEFAULT_VARIABLE_START_TRIGGER)) return name;
|
|
42
|
+
return `${_maily_to_shared.DEFAULT_VARIABLE_START_TRIGGER}${name}${_maily_to_shared.DEFAULT_VARIABLE_END_TRIGGER}`;
|
|
33
43
|
}
|
|
34
44
|
//#endregion
|
|
35
45
|
//#region src/transforms/button.ts
|
|
36
|
-
const
|
|
37
|
-
sharp:
|
|
38
|
-
smooth:
|
|
39
|
-
round:
|
|
46
|
+
const BORDER_RADIUS_VALUES = {
|
|
47
|
+
sharp: "0px",
|
|
48
|
+
smooth: "6px",
|
|
49
|
+
round: "9999px"
|
|
40
50
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
* node when `isTextVariable` is true).
|
|
46
|
-
* - Renames `buttonColor` → `backgroundColor`, `textColor` → `color`.
|
|
47
|
-
* - Converts `variant: 'outline'` to `backgroundColor: 'transparent'`.
|
|
48
|
-
* - Converts the `borderRadius` string enum (sharp/smooth/round) into
|
|
49
|
-
* four numeric corner properties via splitBorderRadius.
|
|
50
|
-
* - Sets v2 defaults: kind "tight", paddingMode "mixed",
|
|
51
|
-
* borderRadiusMode/borderWidthMode "uniform", borderStyle "solid".
|
|
52
|
-
* - Drops deprecated flags: isTextVariable, isUrlVariable, variant.
|
|
53
|
-
*/
|
|
51
|
+
const FALLBACK_BORDER_RADIUS = "9999px";
|
|
52
|
+
const FALLBACK_OUTLINE_COLOR = "#000000";
|
|
53
|
+
const OUTLINE_VARIANT = "outline";
|
|
54
|
+
const V1_DEFAULT_ALIGN = _maily_to_shared.TEXT_ALIGNMENTS.LEFT;
|
|
54
55
|
function button(node, warnings) {
|
|
55
|
-
|
|
56
|
+
node.attrs ??= {};
|
|
57
|
+
const attrs = node.attrs;
|
|
56
58
|
if (!node.content) {
|
|
57
59
|
const text = attrs.text ?? "";
|
|
58
60
|
if (attrs.isTextVariable && text) node.content = [{
|
|
59
|
-
type:
|
|
61
|
+
type: _maily_to_shared.MAILY_NODE_TYPES.VARIABLE,
|
|
60
62
|
attrs: { id: text }
|
|
61
63
|
}];
|
|
62
64
|
else if (text) node.content = [{
|
|
63
|
-
type:
|
|
65
|
+
type: _maily_to_shared.MAILY_NODE_TYPES.TEXT,
|
|
64
66
|
text
|
|
65
67
|
}];
|
|
66
68
|
}
|
|
@@ -80,90 +82,170 @@ function button(node, warnings) {
|
|
|
80
82
|
});
|
|
81
83
|
delete attrs.isTextVariable;
|
|
82
84
|
}
|
|
83
|
-
if (
|
|
85
|
+
if (attrs.isUrlVariable === true) {
|
|
86
|
+
warnings.push({
|
|
87
|
+
nodeType: "button",
|
|
88
|
+
field: "isUrlVariable",
|
|
89
|
+
message: "Migrated \"isUrlVariable\" → variable reference in \"url\""
|
|
90
|
+
});
|
|
91
|
+
attrs.url = getVariableReference(attrs.url ?? "");
|
|
92
|
+
}
|
|
93
|
+
delete attrs.isUrlVariable;
|
|
94
|
+
if ("alignment" in attrs) {
|
|
95
|
+
if (attrs.alignment) {
|
|
96
|
+
warnings.push({
|
|
97
|
+
nodeType: "button",
|
|
98
|
+
field: "alignment",
|
|
99
|
+
message: "Migrated \"alignment\" → \"align\""
|
|
100
|
+
});
|
|
101
|
+
attrs.align = attrs.alignment;
|
|
102
|
+
}
|
|
103
|
+
delete attrs.alignment;
|
|
104
|
+
}
|
|
105
|
+
attrs.align ??= V1_DEFAULT_ALIGN;
|
|
106
|
+
const declarations = (0, _maily_to_shared.parseInlineStyles)(attrs.style ?? "");
|
|
107
|
+
if (attrs.buttonColor != null && attrs.variant !== OUTLINE_VARIANT) {
|
|
84
108
|
warnings.push({
|
|
85
109
|
nodeType: "button",
|
|
86
110
|
field: "buttonColor",
|
|
87
|
-
message: "Migrated \"buttonColor\" → \"
|
|
111
|
+
message: "Migrated \"buttonColor\" → \"style\""
|
|
88
112
|
});
|
|
89
|
-
|
|
90
|
-
delete attrs.buttonColor;
|
|
113
|
+
declarations.backgroundColor = attrs.buttonColor;
|
|
91
114
|
}
|
|
92
|
-
if (
|
|
115
|
+
if (attrs.textColor != null) {
|
|
93
116
|
warnings.push({
|
|
94
117
|
nodeType: "button",
|
|
95
118
|
field: "textColor",
|
|
96
|
-
message: "Migrated \"textColor\" → \"
|
|
119
|
+
message: "Migrated \"textColor\" → \"style\""
|
|
97
120
|
});
|
|
98
|
-
|
|
99
|
-
delete attrs.textColor;
|
|
121
|
+
declarations.color = attrs.textColor;
|
|
100
122
|
}
|
|
101
|
-
if (attrs.variant ===
|
|
123
|
+
if (attrs.variant === OUTLINE_VARIANT) {
|
|
102
124
|
warnings.push({
|
|
103
125
|
nodeType: "button",
|
|
104
126
|
field: "variant",
|
|
105
|
-
message: "Migrated \"variant: outline\" → transparent
|
|
127
|
+
message: "Migrated \"variant: outline\" → transparent background + border in \"style\""
|
|
106
128
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
attrs.borderLeftWidth = 2;
|
|
112
|
-
attrs.backgroundColor = "transparent";
|
|
129
|
+
declarations.backgroundColor = "transparent";
|
|
130
|
+
declarations.borderWidth = "2px";
|
|
131
|
+
declarations.borderStyle = "solid";
|
|
132
|
+
declarations.borderColor = attrs.buttonColor ?? FALLBACK_OUTLINE_COLOR;
|
|
113
133
|
}
|
|
114
134
|
delete attrs.variant;
|
|
115
135
|
if (typeof attrs.borderRadius === "string") {
|
|
116
136
|
warnings.push({
|
|
117
137
|
nodeType: "button",
|
|
118
138
|
field: "borderRadius",
|
|
119
|
-
message: "Migrated \"borderRadius\" enum →
|
|
120
|
-
});
|
|
121
|
-
const radius = BORDER_RADIUS_MAP[attrs.borderRadius] ?? 9999;
|
|
122
|
-
Object.assign(attrs, splitBorderRadius(radius));
|
|
123
|
-
delete attrs.borderRadius;
|
|
124
|
-
}
|
|
125
|
-
attrs.kind ??= "tight";
|
|
126
|
-
attrs.paddingMode ??= "mixed";
|
|
127
|
-
attrs.borderRadiusMode ??= "uniform";
|
|
128
|
-
attrs.borderWidthMode ??= "uniform";
|
|
129
|
-
attrs.borderStyle ??= "solid";
|
|
130
|
-
attrs.borderColor ??= "#000000";
|
|
131
|
-
attrs.borderTopWidth ??= 0;
|
|
132
|
-
attrs.borderRightWidth ??= 0;
|
|
133
|
-
attrs.borderBottomWidth ??= 0;
|
|
134
|
-
attrs.borderLeftWidth ??= 0;
|
|
135
|
-
if ("isUrlVariable" in attrs) {
|
|
136
|
-
warnings.push({
|
|
137
|
-
nodeType: "button",
|
|
138
|
-
field: "isUrlVariable",
|
|
139
|
-
message: "Attribute \"isUrlVariable\" is not supported in v2 and was dropped"
|
|
139
|
+
message: "Migrated \"borderRadius\" enum → \"style\""
|
|
140
140
|
});
|
|
141
|
-
|
|
142
|
-
}
|
|
141
|
+
declarations.borderRadius = BORDER_RADIUS_VALUES[attrs.borderRadius] ?? FALLBACK_BORDER_RADIUS;
|
|
142
|
+
}
|
|
143
|
+
delete attrs.borderRadius;
|
|
144
|
+
Object.assign(declarations, getBoxDeclarations({
|
|
145
|
+
bottom: attrs.paddingBottom,
|
|
146
|
+
left: attrs.paddingLeft,
|
|
147
|
+
property: "padding",
|
|
148
|
+
right: attrs.paddingRight,
|
|
149
|
+
top: attrs.paddingTop
|
|
150
|
+
}));
|
|
151
|
+
delete attrs.paddingTop;
|
|
152
|
+
delete attrs.paddingRight;
|
|
153
|
+
delete attrs.paddingBottom;
|
|
154
|
+
delete attrs.paddingLeft;
|
|
155
|
+
const style = getStyleAttribute(declarations);
|
|
156
|
+
if (style) attrs.style = style;
|
|
157
|
+
attrs.kind ??= _maily_to_shared.DEFAULT_BUTTON_KIND;
|
|
158
|
+
delete attrs.buttonColor;
|
|
159
|
+
delete attrs.textColor;
|
|
143
160
|
}
|
|
144
161
|
//#endregion
|
|
145
|
-
//#region src/
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"borderRadius",
|
|
150
|
-
"borderWidth",
|
|
151
|
-
"borderColor",
|
|
162
|
+
//#region src/utils/box-style.ts
|
|
163
|
+
const DEFAULT_BORDER_COLOR = "#e2e2e2";
|
|
164
|
+
const BORDER_FIELDS = ["borderWidth", "borderColor"];
|
|
165
|
+
const PADDING_FIELDS = [
|
|
152
166
|
"paddingTop",
|
|
153
167
|
"paddingRight",
|
|
154
168
|
"paddingBottom",
|
|
155
|
-
"paddingLeft"
|
|
156
|
-
"visibilityRule"
|
|
169
|
+
"paddingLeft"
|
|
157
170
|
];
|
|
171
|
+
function getBorderDeclarations(options) {
|
|
172
|
+
const { color, width } = options;
|
|
173
|
+
if (typeof width !== "number" || width <= 0) return {};
|
|
174
|
+
return {
|
|
175
|
+
borderColor: color ?? DEFAULT_BORDER_COLOR,
|
|
176
|
+
borderStyle: "solid",
|
|
177
|
+
borderWidth: getPixelValue(width)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function getBorderRadiusDeclarations(radius) {
|
|
181
|
+
if (typeof radius !== "number" || radius <= 0) return {};
|
|
182
|
+
return { borderRadius: getPixelValue(radius) };
|
|
183
|
+
}
|
|
184
|
+
function getBoxStyleDeclarations(options) {
|
|
185
|
+
const { attrs, nodeType, warnings } = options;
|
|
186
|
+
const declarations = {};
|
|
187
|
+
if (attrs.backgroundColor) {
|
|
188
|
+
warnings.push({
|
|
189
|
+
nodeType,
|
|
190
|
+
field: "backgroundColor",
|
|
191
|
+
message: "Migrated \"backgroundColor\" → \"style\""
|
|
192
|
+
});
|
|
193
|
+
declarations.backgroundColor = attrs.backgroundColor;
|
|
194
|
+
}
|
|
195
|
+
delete attrs.backgroundColor;
|
|
196
|
+
const border = getBorderDeclarations({
|
|
197
|
+
color: attrs.borderColor,
|
|
198
|
+
width: attrs.borderWidth
|
|
199
|
+
});
|
|
200
|
+
if (Object.keys(border).length > 0) {
|
|
201
|
+
for (const field of BORDER_FIELDS) if (attrs[field] != null) warnings.push({
|
|
202
|
+
nodeType,
|
|
203
|
+
field,
|
|
204
|
+
message: `Migrated "${field}" → "style"`
|
|
205
|
+
});
|
|
206
|
+
Object.assign(declarations, border);
|
|
207
|
+
}
|
|
208
|
+
for (const field of BORDER_FIELDS) delete attrs[field];
|
|
209
|
+
const radius = getBorderRadiusDeclarations(attrs.borderRadius);
|
|
210
|
+
if (Object.keys(radius).length > 0) {
|
|
211
|
+
warnings.push({
|
|
212
|
+
nodeType,
|
|
213
|
+
field: "borderRadius",
|
|
214
|
+
message: "Migrated \"borderRadius\" → \"style\""
|
|
215
|
+
});
|
|
216
|
+
Object.assign(declarations, radius);
|
|
217
|
+
}
|
|
218
|
+
delete attrs.borderRadius;
|
|
219
|
+
const padding = getBoxDeclarations({
|
|
220
|
+
bottom: attrs.paddingBottom,
|
|
221
|
+
left: attrs.paddingLeft,
|
|
222
|
+
property: "padding",
|
|
223
|
+
right: attrs.paddingRight,
|
|
224
|
+
top: attrs.paddingTop
|
|
225
|
+
});
|
|
226
|
+
if (Object.keys(padding).length > 0) {
|
|
227
|
+
warnings.push({
|
|
228
|
+
nodeType,
|
|
229
|
+
field: "padding",
|
|
230
|
+
message: "Migrated \"padding*\" → \"style\""
|
|
231
|
+
});
|
|
232
|
+
Object.assign(declarations, padding);
|
|
233
|
+
}
|
|
234
|
+
for (const field of PADDING_FIELDS) delete attrs[field];
|
|
235
|
+
return declarations;
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/transforms/column.ts
|
|
239
|
+
const DROPPED_COLUMN_ATTRS = ["columnId", "visibilityRule"];
|
|
158
240
|
/**
|
|
159
241
|
* Migrates a v1 column node to the v2 schema.
|
|
160
242
|
* Performs the following conversions:
|
|
161
243
|
* - Converts `width: 'auto'` → `null` (v2 uses null for equal-space columns).
|
|
162
244
|
* - Converts percentage width strings (e.g. "50%") to numbers (e.g. 50).
|
|
163
|
-
* -
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
245
|
+
* - Converts the v1 styling attributes backgroundColor, borderRadius,
|
|
246
|
+
* borderWidth/borderColor and paddingTop/Right/Bottom/Left into `style`.
|
|
247
|
+
* - Drops columnId and visibilityRule, neither of which a v2 column
|
|
248
|
+
* supports. Each dropped attribute emits a warning.
|
|
167
249
|
*/
|
|
168
250
|
function column(node, warnings) {
|
|
169
251
|
const attrs = node.attrs ?? {};
|
|
@@ -172,6 +254,15 @@ function column(node, warnings) {
|
|
|
172
254
|
const parsed = parseFloat(attrs.width);
|
|
173
255
|
attrs.width = Number.isNaN(parsed) ? null : parsed;
|
|
174
256
|
}
|
|
257
|
+
const style = getStyleAttribute({
|
|
258
|
+
...(0, _maily_to_shared.parseInlineStyles)(attrs.style ?? ""),
|
|
259
|
+
...getBoxStyleDeclarations({
|
|
260
|
+
attrs,
|
|
261
|
+
nodeType: "column",
|
|
262
|
+
warnings
|
|
263
|
+
})
|
|
264
|
+
});
|
|
265
|
+
if (style) attrs.style = style;
|
|
175
266
|
for (const field of DROPPED_COLUMN_ATTRS) if (field in attrs && attrs[field] != null) {
|
|
176
267
|
warnings.push({
|
|
177
268
|
nodeType: "column",
|
|
@@ -183,6 +274,7 @@ function column(node, warnings) {
|
|
|
183
274
|
}
|
|
184
275
|
//#endregion
|
|
185
276
|
//#region src/transforms/columns.ts
|
|
277
|
+
const DEFAULT_COLUMNS_GAP$1 = 8;
|
|
186
278
|
/**
|
|
187
279
|
* Migrates a v1 columns node to the v2 schema.
|
|
188
280
|
* In v2, columns tracks its child count and gap explicitly:
|
|
@@ -192,16 +284,10 @@ function column(node, warnings) {
|
|
|
192
284
|
function columns(node, _warnings) {
|
|
193
285
|
const attrs = node.attrs ?? {};
|
|
194
286
|
attrs.columnCount ??= node.content?.length ?? 0;
|
|
195
|
-
attrs.gap ??=
|
|
287
|
+
attrs.gap ??= DEFAULT_COLUMNS_GAP$1;
|
|
196
288
|
}
|
|
197
289
|
//#endregion
|
|
198
290
|
//#region src/transforms/footer.ts
|
|
199
|
-
/**
|
|
200
|
-
* Migrates a v1 footer node to the v2 schema.
|
|
201
|
-
* Removes the `maily-component` attribute which was used in v1
|
|
202
|
-
* as a marker to identify the footer node. In v2, the node type
|
|
203
|
-
* alone is sufficient for identification.
|
|
204
|
-
*/
|
|
205
291
|
function footer(node, warnings) {
|
|
206
292
|
const attrs = node.attrs ?? {};
|
|
207
293
|
if ("maily-component" in attrs) {
|
|
@@ -214,30 +300,22 @@ function footer(node, warnings) {
|
|
|
214
300
|
}
|
|
215
301
|
}
|
|
216
302
|
//#endregion
|
|
217
|
-
//#region src/utils/id.ts
|
|
218
|
-
/**
|
|
219
|
-
* Generates a unique identifier for a node.
|
|
220
|
-
* Uses the Web Crypto API to produce a v4 UUID string,
|
|
221
|
-
* e.g. "3b241101-e2bb-4d7a-8613-e4d4e2f1b9c1".
|
|
222
|
-
*/
|
|
223
|
-
function uid() {
|
|
224
|
-
return crypto.randomUUID();
|
|
225
|
-
}
|
|
226
|
-
//#endregion
|
|
227
303
|
//#region src/transforms/global.ts
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
304
|
+
const ID_NODE_TYPES = [
|
|
305
|
+
_maily_to_shared.MAILY_NODE_TYPES.HEADING,
|
|
306
|
+
_maily_to_shared.MAILY_NODE_TYPES.PARAGRAPH,
|
|
307
|
+
_maily_to_shared.MAILY_NODE_TYPES.SECTION,
|
|
308
|
+
_maily_to_shared.MAILY_NODE_TYPES.REPEAT,
|
|
309
|
+
_maily_to_shared.MAILY_NODE_TYPES.COLUMNS,
|
|
310
|
+
_maily_to_shared.MAILY_NODE_TYPES.COLUMN,
|
|
311
|
+
_maily_to_shared.MAILY_NODE_TYPES.BULLET_LIST,
|
|
312
|
+
_maily_to_shared.MAILY_NODE_TYPES.ORDERED_LIST,
|
|
313
|
+
_maily_to_shared.MAILY_NODE_TYPES.LIST_ITEM,
|
|
314
|
+
_maily_to_shared.MAILY_NODE_TYPES.HORIZONTAL_RULE,
|
|
315
|
+
_maily_to_shared.MAILY_NODE_TYPES.BLOCKQUOTE
|
|
316
|
+
];
|
|
239
317
|
function global(node, warnings) {
|
|
240
|
-
if (node.type ===
|
|
318
|
+
if (node.type === _maily_to_shared.MAILY_NODE_TYPES.TEXT) return;
|
|
241
319
|
if (!node.attrs) node.attrs = {};
|
|
242
320
|
const attrs = node.attrs;
|
|
243
321
|
if (attrs.showIfKey) {
|
|
@@ -265,39 +343,44 @@ function global(node, warnings) {
|
|
|
265
343
|
}
|
|
266
344
|
delete attrs.textDirection;
|
|
267
345
|
}
|
|
268
|
-
if (
|
|
346
|
+
if ("textAlign" in attrs) {
|
|
347
|
+
if (attrs.textAlign && attrs.textAlign !== "left") {
|
|
348
|
+
warnings.push({
|
|
349
|
+
nodeType: node.type,
|
|
350
|
+
field: "textAlign",
|
|
351
|
+
message: "Migrated \"textAlign\" → \"style\""
|
|
352
|
+
});
|
|
353
|
+
const declarations = (0, _maily_to_shared.parseInlineStyles)(attrs.style ?? "");
|
|
354
|
+
declarations.textAlign = attrs.textAlign;
|
|
355
|
+
attrs.style = (0, _maily_to_shared.stringifyInlineStyles)(declarations);
|
|
356
|
+
}
|
|
357
|
+
delete attrs.textAlign;
|
|
358
|
+
}
|
|
359
|
+
if (ID_NODE_TYPES.includes(node.type) && !attrs.id) attrs.id = crypto.randomUUID();
|
|
269
360
|
}
|
|
270
361
|
//#endregion
|
|
271
|
-
//#region src/
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
* persisted into the document JSON.
|
|
276
|
-
*/
|
|
277
|
-
function htmlCodeBlock(node, warnings) {
|
|
278
|
-
const attrs = node.attrs ?? {};
|
|
279
|
-
if ("activeTab" in attrs) {
|
|
280
|
-
warnings.push({
|
|
281
|
-
nodeType: "htmlCodeBlock",
|
|
282
|
-
field: "activeTab",
|
|
283
|
-
message: "Editor-only attribute \"activeTab\" is not supported in v2 and was dropped"
|
|
284
|
-
});
|
|
285
|
-
delete attrs.activeTab;
|
|
286
|
-
}
|
|
362
|
+
//#region src/utils/image-width.ts
|
|
363
|
+
function getWidthPercentage(options) {
|
|
364
|
+
const { containerWidth, pixels } = options;
|
|
365
|
+
return (0, _maily_to_shared.clampImageWidthPercentage)(Math.round(pixels / containerWidth * 100));
|
|
287
366
|
}
|
|
288
367
|
//#endregion
|
|
289
368
|
//#region src/transforms/image.ts
|
|
290
|
-
const
|
|
369
|
+
const DROPPED_IMAGE_ATTRS = [
|
|
370
|
+
"height",
|
|
371
|
+
"lockAspectRatio",
|
|
372
|
+
"aspectRatio"
|
|
373
|
+
];
|
|
291
374
|
/**
|
|
292
375
|
* Migrates a v1 image node to the v2 schema.
|
|
293
376
|
* Performs the following conversions:
|
|
294
377
|
* - Renames `alignment` → `align` to match the v2 attribute name.
|
|
295
|
-
* -
|
|
296
|
-
* - Converts `width
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
378
|
+
* - Moves the `borderRadius` number into the `style` attribute.
|
|
379
|
+
* - Converts `width` to a percentage string, using the container width
|
|
380
|
+
* stamped by `annotateContainerWidths` to scale pixel values.
|
|
381
|
+
* - Resolves `isSrcVariable` / `isExternalLinkVariable` into variable
|
|
382
|
+
* references inside `src` / `externalLink`.
|
|
383
|
+
* - Drops deprecated attrs: height, lockAspectRatio, aspectRatio.
|
|
301
384
|
*/
|
|
302
385
|
function image(node, warnings) {
|
|
303
386
|
const attrs = node.attrs ?? {};
|
|
@@ -310,37 +393,45 @@ function image(node, warnings) {
|
|
|
310
393
|
attrs.align = attrs.alignment;
|
|
311
394
|
delete attrs.alignment;
|
|
312
395
|
}
|
|
313
|
-
|
|
396
|
+
const declarations = {};
|
|
397
|
+
if (typeof attrs.borderRadius === "number" && attrs.borderRadius > 0) {
|
|
314
398
|
warnings.push({
|
|
315
399
|
nodeType: "image",
|
|
316
400
|
field: "borderRadius",
|
|
317
|
-
message: "Migrated \"borderRadius\" →
|
|
401
|
+
message: "Migrated \"borderRadius\" → \"style\""
|
|
318
402
|
});
|
|
319
|
-
|
|
320
|
-
delete attrs.borderRadius;
|
|
403
|
+
declarations.borderRadius = getPixelValue(attrs.borderRadius);
|
|
321
404
|
}
|
|
322
|
-
|
|
405
|
+
delete attrs.borderRadius;
|
|
406
|
+
const containerWidth = attrs._containerWidth ?? _maily_to_shared.DEFAULT_CONTAINER_WIDTH_PIXELS;
|
|
323
407
|
delete attrs._containerWidth;
|
|
324
408
|
if (attrs.width === "auto") attrs.width = "100%";
|
|
325
409
|
else if (typeof attrs.width === "number" || typeof attrs.width === "string" && !attrs.width.endsWith("%")) {
|
|
326
|
-
const
|
|
327
|
-
if (!Number.isNaN(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
attrs.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
410
|
+
const pixels = Number(attrs.width);
|
|
411
|
+
if (!Number.isNaN(pixels) && pixels > 0) attrs.width = `${getWidthPercentage({
|
|
412
|
+
containerWidth,
|
|
413
|
+
pixels
|
|
414
|
+
})}%`;
|
|
415
|
+
}
|
|
416
|
+
if (attrs.isSrcVariable === true) {
|
|
417
|
+
warnings.push({
|
|
418
|
+
nodeType: "image",
|
|
419
|
+
field: "isSrcVariable",
|
|
420
|
+
message: "Migrated \"isSrcVariable\" → variable reference in \"src\""
|
|
421
|
+
});
|
|
422
|
+
attrs.src = getVariableReference(attrs.src);
|
|
423
|
+
}
|
|
424
|
+
delete attrs.isSrcVariable;
|
|
425
|
+
if (attrs.isExternalLinkVariable === true && attrs.externalLink) {
|
|
426
|
+
warnings.push({
|
|
427
|
+
nodeType: "image",
|
|
428
|
+
field: "isExternalLinkVariable",
|
|
429
|
+
message: "Migrated \"isExternalLinkVariable\" → variable reference in \"externalLink\""
|
|
430
|
+
});
|
|
431
|
+
attrs.externalLink = getVariableReference(attrs.externalLink);
|
|
432
|
+
}
|
|
433
|
+
delete attrs.isExternalLinkVariable;
|
|
434
|
+
for (const field of DROPPED_IMAGE_ATTRS) if (field in attrs) {
|
|
344
435
|
warnings.push({
|
|
345
436
|
nodeType: "image",
|
|
346
437
|
field,
|
|
@@ -348,80 +439,118 @@ function image(node, warnings) {
|
|
|
348
439
|
});
|
|
349
440
|
delete attrs[field];
|
|
350
441
|
}
|
|
442
|
+
const style = getStyleAttribute({
|
|
443
|
+
...(0, _maily_to_shared.parseInlineStyles)(attrs.style ?? ""),
|
|
444
|
+
...declarations
|
|
445
|
+
});
|
|
446
|
+
if (style) attrs.style = style;
|
|
351
447
|
}
|
|
352
448
|
//#endregion
|
|
353
449
|
//#region src/transforms/inline-image.ts
|
|
354
|
-
/**
|
|
355
|
-
* Migrates a v1 inlineImage node to the v2 schema.
|
|
356
|
-
* Performs the following conversions:
|
|
357
|
-
* - Converts `src: null` or `src: undefined` to an empty string,
|
|
358
|
-
* since v2 expects src to always be a string.
|
|
359
|
-
* - Drops deprecated flags: isSrcVariable, isExternalLinkVariable.
|
|
360
|
-
*/
|
|
361
450
|
function inlineImage(node, warnings) {
|
|
362
451
|
const attrs = node.attrs ?? {};
|
|
363
452
|
if (attrs.src == null) attrs.src = "";
|
|
364
|
-
|
|
453
|
+
if (attrs.isSrcVariable === true) {
|
|
365
454
|
warnings.push({
|
|
366
455
|
nodeType: "inlineImage",
|
|
367
|
-
field,
|
|
368
|
-
message:
|
|
456
|
+
field: "isSrcVariable",
|
|
457
|
+
message: "Migrated \"isSrcVariable\" → variable reference in \"src\""
|
|
369
458
|
});
|
|
370
|
-
|
|
459
|
+
attrs.src = getVariableReference(attrs.src);
|
|
371
460
|
}
|
|
461
|
+
delete attrs.isSrcVariable;
|
|
462
|
+
if (attrs.isExternalLinkVariable === true && attrs.externalLink) {
|
|
463
|
+
warnings.push({
|
|
464
|
+
nodeType: "inlineImage",
|
|
465
|
+
field: "isExternalLinkVariable",
|
|
466
|
+
message: "Migrated \"isExternalLinkVariable\" → variable reference in \"externalLink\""
|
|
467
|
+
});
|
|
468
|
+
attrs.externalLink = getVariableReference(attrs.externalLink);
|
|
469
|
+
}
|
|
470
|
+
delete attrs.isExternalLinkVariable;
|
|
372
471
|
}
|
|
373
472
|
//#endregion
|
|
374
473
|
//#region src/transforms/link.ts
|
|
375
|
-
/**
|
|
376
|
-
* Migrates a v1 link mark to the v2 schema.
|
|
377
|
-
* Removes the `isUrlVariable` flag from the mark attrs.
|
|
378
|
-
* In v1, this boolean indicated whether the href was a variable
|
|
379
|
-
* reference; v2 handles variable detection differently and no
|
|
380
|
-
* longer needs this flag.
|
|
381
|
-
*/
|
|
382
474
|
function link(mark, warnings) {
|
|
383
475
|
const attrs = mark.attrs ?? {};
|
|
384
|
-
if (
|
|
476
|
+
if (attrs.isUrlVariable === true) {
|
|
385
477
|
warnings.push({
|
|
386
478
|
nodeType: "link",
|
|
387
479
|
field: "isUrlVariable",
|
|
388
|
-
message: "
|
|
480
|
+
message: "Migrated \"isUrlVariable\" → variable reference in \"href\""
|
|
389
481
|
});
|
|
390
|
-
|
|
482
|
+
attrs.href = getVariableReference(attrs.href ?? "");
|
|
483
|
+
}
|
|
484
|
+
delete attrs.isUrlVariable;
|
|
485
|
+
}
|
|
486
|
+
//#endregion
|
|
487
|
+
//#region src/transforms/link-card.ts
|
|
488
|
+
function linkCard(node, warnings) {
|
|
489
|
+
const attrs = node.attrs ?? {};
|
|
490
|
+
if ("mailyComponent" in attrs) {
|
|
491
|
+
warnings.push({
|
|
492
|
+
nodeType: "linkCard",
|
|
493
|
+
field: "mailyComponent",
|
|
494
|
+
message: "v1 marker attribute \"mailyComponent\" is not needed in v2 and was dropped"
|
|
495
|
+
});
|
|
496
|
+
delete attrs.mailyComponent;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/transforms/list-item.ts
|
|
501
|
+
function listItem(node, warnings) {
|
|
502
|
+
const attrs = node.attrs ?? {};
|
|
503
|
+
if (attrs.color != null) {
|
|
504
|
+
const style = getStyleAttribute({ color: attrs.color });
|
|
505
|
+
if (style) {
|
|
506
|
+
warnings.push({
|
|
507
|
+
nodeType: "listItem",
|
|
508
|
+
field: "color",
|
|
509
|
+
message: "Migrated \"color\" → \"style\""
|
|
510
|
+
});
|
|
511
|
+
attrs.style = (0, _maily_to_shared.stringifyInlineStyles)({
|
|
512
|
+
...(0, _maily_to_shared.parseInlineStyles)(attrs.style ?? ""),
|
|
513
|
+
...(0, _maily_to_shared.parseInlineStyles)(style)
|
|
514
|
+
});
|
|
515
|
+
}
|
|
391
516
|
}
|
|
517
|
+
delete attrs.color;
|
|
392
518
|
}
|
|
393
519
|
//#endregion
|
|
394
520
|
//#region src/transforms/logo.ts
|
|
395
|
-
const
|
|
521
|
+
const LOGO_SIZE_PIXELS = {
|
|
396
522
|
sm: 40,
|
|
397
523
|
md: 48,
|
|
398
524
|
lg: 64
|
|
399
525
|
};
|
|
400
|
-
const
|
|
526
|
+
const DEFAULT_LOGO_SIZE_PIXELS = 48;
|
|
401
527
|
/**
|
|
402
528
|
* Migrates a v1 logo node by converting it into a v2 image node.
|
|
403
529
|
* The logo node type was removed in v2 — logos are now just images.
|
|
404
530
|
* Performs the following conversions:
|
|
405
531
|
* - Changes `type: 'logo'` → `type: 'image'`.
|
|
406
|
-
* - Maps the `size` enum (sm/md/lg) to a
|
|
407
|
-
*
|
|
532
|
+
* - Maps the `size` enum (sm/md/lg → 40/48/64 pixels) to a percentage
|
|
533
|
+
* `width`, scaled against the container width stamped by
|
|
534
|
+
* `annotateContainerWidths`.
|
|
408
535
|
* - Renames `alignment` → `align`.
|
|
409
|
-
* -
|
|
410
|
-
* - Drops
|
|
536
|
+
* - Resolves `isSrcVariable` into a variable reference inside `src`.
|
|
537
|
+
* - Drops the `maily-component` marker.
|
|
411
538
|
*/
|
|
412
539
|
function logo(node, warnings) {
|
|
413
540
|
node.type = "image";
|
|
414
541
|
const attrs = node.attrs ?? {};
|
|
415
542
|
if ("size" in attrs) {
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
|
|
543
|
+
const pixels = LOGO_SIZE_PIXELS[attrs.size] ?? DEFAULT_LOGO_SIZE_PIXELS;
|
|
544
|
+
const percentage = getWidthPercentage({
|
|
545
|
+
containerWidth: attrs._containerWidth ?? _maily_to_shared.DEFAULT_CONTAINER_WIDTH_PIXELS,
|
|
546
|
+
pixels
|
|
547
|
+
});
|
|
419
548
|
warnings.push({
|
|
420
549
|
nodeType: "logo",
|
|
421
550
|
field: "size",
|
|
422
|
-
message: `Migrated "size: ${attrs.size}" → "width: ${
|
|
551
|
+
message: `Migrated "size: ${attrs.size}" → "width: ${percentage}%"`
|
|
423
552
|
});
|
|
424
|
-
attrs.width = `${
|
|
553
|
+
attrs.width = `${percentage}%`;
|
|
425
554
|
delete attrs.size;
|
|
426
555
|
}
|
|
427
556
|
delete attrs._containerWidth;
|
|
@@ -434,37 +563,35 @@ function logo(node, warnings) {
|
|
|
434
563
|
attrs.align = attrs.alignment;
|
|
435
564
|
delete attrs.alignment;
|
|
436
565
|
}
|
|
437
|
-
attrs.
|
|
438
|
-
attrs.borderWidthMode ??= "uniform";
|
|
439
|
-
attrs.borderStyle ??= "solid";
|
|
440
|
-
attrs.borderColor ??= "#000000";
|
|
441
|
-
attrs.borderTopWidth ??= 0;
|
|
442
|
-
attrs.borderRightWidth ??= 0;
|
|
443
|
-
attrs.borderBottomWidth ??= 0;
|
|
444
|
-
attrs.borderLeftWidth ??= 0;
|
|
445
|
-
attrs.borderTopLeftRadius ??= 0;
|
|
446
|
-
attrs.borderTopRightRadius ??= 0;
|
|
447
|
-
attrs.borderBottomRightRadius ??= 0;
|
|
448
|
-
attrs.borderBottomLeftRadius ??= 0;
|
|
449
|
-
for (const field of ["isSrcVariable", "maily-component"]) if (field in attrs) {
|
|
566
|
+
if (attrs.isSrcVariable === true) {
|
|
450
567
|
warnings.push({
|
|
451
568
|
nodeType: "logo",
|
|
452
|
-
field,
|
|
453
|
-
message:
|
|
569
|
+
field: "isSrcVariable",
|
|
570
|
+
message: "Migrated \"isSrcVariable\" → variable reference in \"src\""
|
|
454
571
|
});
|
|
455
|
-
|
|
572
|
+
attrs.src = getVariableReference(attrs.src);
|
|
573
|
+
}
|
|
574
|
+
delete attrs.isSrcVariable;
|
|
575
|
+
if ("maily-component" in attrs) {
|
|
576
|
+
warnings.push({
|
|
577
|
+
nodeType: "logo",
|
|
578
|
+
field: "maily-component",
|
|
579
|
+
message: "Attribute \"maily-component\" is not supported in v2 and was dropped"
|
|
580
|
+
});
|
|
581
|
+
delete attrs["maily-component"];
|
|
456
582
|
}
|
|
457
583
|
}
|
|
458
584
|
//#endregion
|
|
459
585
|
//#region src/transforms/repeat.ts
|
|
460
|
-
/**
|
|
461
|
-
* Migrates a v1 repeat node to the v2 schema.
|
|
462
|
-
* Wraps the `each` attribute value in Handlebars-style template syntax
|
|
463
|
-
* if it isn't already wrapped. For example, `'items'` becomes `'{{items}}'`.
|
|
464
|
-
* In v2, the each value must be a template expression; v1 allowed
|
|
465
|
-
* bare variable names without delimiters.
|
|
466
|
-
*/
|
|
467
586
|
function repeat(node, warnings) {
|
|
587
|
+
if (node.type === "for") {
|
|
588
|
+
warnings.push({
|
|
589
|
+
nodeType: "for",
|
|
590
|
+
field: "type",
|
|
591
|
+
message: "Migrated deprecated \"for\" node → \"repeat\""
|
|
592
|
+
});
|
|
593
|
+
node.type = "repeat";
|
|
594
|
+
}
|
|
468
595
|
const attrs = node.attrs ?? {};
|
|
469
596
|
if (typeof attrs.each === "string" && attrs.each && !attrs.each.startsWith("{{")) {
|
|
470
597
|
warnings.push({
|
|
@@ -485,92 +612,92 @@ function repeat(node, warnings) {
|
|
|
485
612
|
}
|
|
486
613
|
//#endregion
|
|
487
614
|
//#region src/transforms/section.ts
|
|
615
|
+
const MARGIN_FIELDS = [
|
|
616
|
+
"marginTop",
|
|
617
|
+
"marginRight",
|
|
618
|
+
"marginBottom",
|
|
619
|
+
"marginLeft"
|
|
620
|
+
];
|
|
488
621
|
/**
|
|
489
622
|
* Migrates a v1 section node to the v2 schema.
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* -
|
|
493
|
-
* -
|
|
494
|
-
*
|
|
623
|
+
* v1 stored its look as separate attributes and rendered them verbatim, so
|
|
624
|
+
* every one of them becomes an explicit declaration in `style`:
|
|
625
|
+
* - backgroundColor, borderWidth/borderColor and borderRadius.
|
|
626
|
+
* - paddingTop/Right/Bottom/Left and marginTop/Right/Bottom/Left, zeros
|
|
627
|
+
* included, because the v2 section theme otherwise adds 8px of padding.
|
|
628
|
+
* - align → text-align, unless it is the v1 default "left".
|
|
495
629
|
*/
|
|
496
630
|
function section(node, warnings) {
|
|
497
631
|
const attrs = node.attrs ?? {};
|
|
498
|
-
|
|
632
|
+
const declarations = {
|
|
633
|
+
...(0, _maily_to_shared.parseInlineStyles)(attrs.style ?? ""),
|
|
634
|
+
...getBoxStyleDeclarations({
|
|
635
|
+
attrs,
|
|
636
|
+
nodeType: "section",
|
|
637
|
+
warnings
|
|
638
|
+
})
|
|
639
|
+
};
|
|
640
|
+
const margin = getBoxDeclarations({
|
|
641
|
+
bottom: attrs.marginBottom,
|
|
642
|
+
left: attrs.marginLeft,
|
|
643
|
+
property: "margin",
|
|
644
|
+
right: attrs.marginRight,
|
|
645
|
+
top: attrs.marginTop
|
|
646
|
+
});
|
|
647
|
+
if (Object.keys(margin).length > 0) {
|
|
499
648
|
warnings.push({
|
|
500
649
|
nodeType: "section",
|
|
501
|
-
field: "
|
|
502
|
-
message: "Migrated \"
|
|
650
|
+
field: "margin",
|
|
651
|
+
message: "Migrated \"margin*\" → \"style\""
|
|
503
652
|
});
|
|
504
|
-
Object.assign(
|
|
505
|
-
delete attrs.borderRadius;
|
|
653
|
+
Object.assign(declarations, margin);
|
|
506
654
|
}
|
|
507
|
-
|
|
655
|
+
for (const field of MARGIN_FIELDS) delete attrs[field];
|
|
656
|
+
if (attrs.align && attrs.align !== "left") {
|
|
508
657
|
warnings.push({
|
|
509
658
|
nodeType: "section",
|
|
510
|
-
field: "
|
|
511
|
-
message: "Migrated \"
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
attrs.
|
|
518
|
-
attrs.paddingMode ??= "uniform";
|
|
519
|
-
attrs.marginMode ??= "mixed";
|
|
520
|
-
attrs.borderStyle ??= "solid";
|
|
521
|
-
attrs.borderColor ??= "#000000";
|
|
659
|
+
field: "align",
|
|
660
|
+
message: "Migrated \"align\" → \"style\""
|
|
661
|
+
});
|
|
662
|
+
declarations.textAlign = attrs.align;
|
|
663
|
+
}
|
|
664
|
+
delete attrs.align;
|
|
665
|
+
const style = getStyleAttribute(declarations);
|
|
666
|
+
if (style) attrs.style = style;
|
|
522
667
|
}
|
|
523
668
|
//#endregion
|
|
524
669
|
//#region src/transforms/spacer.ts
|
|
525
670
|
const DEFAULT_SPACER_HEIGHT = 8;
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
name: "Small",
|
|
534
|
-
short: "sm",
|
|
535
|
-
value: 8
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
name: "Medium",
|
|
539
|
-
short: "md",
|
|
540
|
-
value: 16
|
|
541
|
-
},
|
|
542
|
-
{
|
|
543
|
-
name: "Large",
|
|
544
|
-
short: "lg",
|
|
545
|
-
value: 32
|
|
546
|
-
},
|
|
547
|
-
{
|
|
548
|
-
name: "Extra Large",
|
|
549
|
-
short: "xl",
|
|
550
|
-
value: 64
|
|
551
|
-
}
|
|
552
|
-
];
|
|
553
|
-
const ALLOWED_SPACING_SHORT_NAMES = SPACING.map((s) => s.short);
|
|
554
|
-
/**
|
|
555
|
-
* Migrates a v1 spacer node to the v2 schema.
|
|
556
|
-
* Sets `heightMode` to "uniform" if not already present.
|
|
557
|
-
* In v2, spacers support per-side height control via heightMode;
|
|
558
|
-
* v1 spacers only had a single uniform height value.
|
|
559
|
-
*/
|
|
671
|
+
const SPACER_ENUM_HEIGHTS = {
|
|
672
|
+
xs: 4,
|
|
673
|
+
sm: 8,
|
|
674
|
+
md: 16,
|
|
675
|
+
lg: 32,
|
|
676
|
+
xl: 64
|
|
677
|
+
};
|
|
560
678
|
function spacer(node, warnings) {
|
|
561
679
|
const attrs = node.attrs ?? {};
|
|
562
|
-
|
|
563
|
-
if (typeof height === "
|
|
564
|
-
|
|
680
|
+
const height = attrs.height;
|
|
681
|
+
if (typeof height === "number") return;
|
|
682
|
+
if (typeof height === "string" && height in SPACER_ENUM_HEIGHTS) {
|
|
683
|
+
const value = SPACER_ENUM_HEIGHTS[height];
|
|
565
684
|
warnings.push({
|
|
566
685
|
nodeType: "spacer",
|
|
567
686
|
field: "height",
|
|
568
|
-
message: `Migrated "height" from string enum to number: ${height} → ${
|
|
687
|
+
message: `Migrated "height" from string enum to number: ${height} → ${value}`
|
|
569
688
|
});
|
|
570
|
-
height =
|
|
689
|
+
attrs.height = value;
|
|
690
|
+
return;
|
|
571
691
|
}
|
|
572
|
-
attrs.height =
|
|
573
|
-
|
|
692
|
+
attrs.height = DEFAULT_SPACER_HEIGHT;
|
|
693
|
+
}
|
|
694
|
+
//#endregion
|
|
695
|
+
//#region src/transforms/variable.ts
|
|
696
|
+
function variable(node) {
|
|
697
|
+
const attrs = node.attrs;
|
|
698
|
+
if (!attrs || !("hideDefaultValue" in attrs)) return;
|
|
699
|
+
attrs.isDefaultEditable = attrs.hideDefaultValue !== true;
|
|
700
|
+
delete attrs.hideDefaultValue;
|
|
574
701
|
}
|
|
575
702
|
//#endregion
|
|
576
703
|
//#region src/utils/container-width.ts
|
|
@@ -629,8 +756,10 @@ function annotateContainerWidths(node, availableWidth = ROOT_WIDTH) {
|
|
|
629
756
|
const col = columns[i];
|
|
630
757
|
const colPercent = parseColumnWidth(col.attrs?.width) ?? autoPercent;
|
|
631
758
|
let gapDeduction = 0;
|
|
632
|
-
if (columnCount > 1)
|
|
633
|
-
|
|
759
|
+
if (columnCount > 1) {
|
|
760
|
+
if (i === 0 || i === columnCount - 1) gapDeduction = gap / 2;
|
|
761
|
+
else gapDeduction = gap;
|
|
762
|
+
}
|
|
634
763
|
const colWidth = childWidth * (colPercent / 100) - gapDeduction;
|
|
635
764
|
annotateContainerWidths(col, Math.max(0, colWidth));
|
|
636
765
|
}
|
|
@@ -642,7 +771,6 @@ function annotateContainerWidths(node, availableWidth = ROOT_WIDTH) {
|
|
|
642
771
|
//#region src/migrate.ts
|
|
643
772
|
const NODE_TRANSFORMS = {
|
|
644
773
|
button,
|
|
645
|
-
htmlCodeBlock,
|
|
646
774
|
image,
|
|
647
775
|
logo,
|
|
648
776
|
section,
|
|
@@ -650,8 +778,12 @@ const NODE_TRANSFORMS = {
|
|
|
650
778
|
column,
|
|
651
779
|
spacer,
|
|
652
780
|
repeat,
|
|
781
|
+
for: repeat,
|
|
653
782
|
footer,
|
|
654
|
-
inlineImage
|
|
783
|
+
inlineImage,
|
|
784
|
+
linkCard,
|
|
785
|
+
listItem,
|
|
786
|
+
variable
|
|
655
787
|
};
|
|
656
788
|
const MARK_TRANSFORMS = { link };
|
|
657
789
|
/**
|
|
@@ -667,9 +799,9 @@ function requireContentMigration(json) {
|
|
|
667
799
|
* Converts a Maily v1 JSON document to the v2 schema.
|
|
668
800
|
* Deep-clones the input so the original is never mutated, then
|
|
669
801
|
* walks the tree iteratively (stack-based) applying global transforms
|
|
670
|
-
* (showIfKey → visibilityRule, textDirection → dir,
|
|
671
|
-
* followed by node-specific transforms (button, image,
|
|
672
|
-
* and mark transforms (link).
|
|
802
|
+
* (showIfKey → visibilityRule, textDirection → dir, textAlign → style,
|
|
803
|
+
* id generation) followed by node-specific transforms (button, image,
|
|
804
|
+
* section, etc.) and mark transforms (link).
|
|
673
805
|
*
|
|
674
806
|
* If the document is already v2+ (version >= 2), it is returned as-is
|
|
675
807
|
* with an empty warnings array. Warnings are emitted when v1 attributes
|