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