@templatical/import-topol 0.46.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +201 -5
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -165,6 +165,197 @@ function parseBorderStyle(value) {
|
|
|
165
165
|
const v = (value ?? "").trim().toLowerCase();
|
|
166
166
|
return v === "dashed" || v === "dotted" ? v : "solid";
|
|
167
167
|
}
|
|
168
|
+
const X_GRID = [
|
|
169
|
+
"left",
|
|
170
|
+
"center",
|
|
171
|
+
"right"
|
|
172
|
+
];
|
|
173
|
+
const Y_GRID = [
|
|
174
|
+
"top",
|
|
175
|
+
"center",
|
|
176
|
+
"bottom"
|
|
177
|
+
];
|
|
178
|
+
const PERCENT = /^(-?(?:\d+(?:\.\d+)?|\.\d+))%$/;
|
|
179
|
+
const LENGTH = /^-?(?:\d+(?:\.\d+)?|\.\d+)[a-z]*$/;
|
|
180
|
+
/** What MJML paints for a `background-*` attribute an `mj-section` / `mj-wrapper` leaves out. */
|
|
181
|
+
const MJML_BACKGROUND_DEFAULTS = {
|
|
182
|
+
size: "auto",
|
|
183
|
+
x: "center",
|
|
184
|
+
y: "top",
|
|
185
|
+
repeat: true
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* `background-size` as a keyword. A size with a side at 100%, or a px width
|
|
189
|
+
* spanning the box, fills it as `cover` does; any other length or percentage
|
|
190
|
+
* draws the image at a size of its own, which `auto` is closest to.
|
|
191
|
+
*/
|
|
192
|
+
function backgroundSizeKeyword(raw, boxWidth) {
|
|
193
|
+
const parts = raw.toLowerCase().split(/\s+/);
|
|
194
|
+
const [first] = parts;
|
|
195
|
+
if (parts.length === 1 && (first === "cover" || first === "contain")) return {
|
|
196
|
+
size: first,
|
|
197
|
+
exact: true
|
|
198
|
+
};
|
|
199
|
+
if (parts.every((part) => part === "auto")) return {
|
|
200
|
+
size: "auto",
|
|
201
|
+
exact: true
|
|
202
|
+
};
|
|
203
|
+
const px = /^(\d+(?:\.\d+)?|\.\d+)px$/.exec(first);
|
|
204
|
+
return {
|
|
205
|
+
size: parts.includes("100%") || px !== null && boxWidth !== void 0 && Number(px[1]) >= boxWidth ? "cover" : "auto",
|
|
206
|
+
exact: false
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* `background-position` split into x and y the way MJML and CSS read it: a
|
|
211
|
+
* lone `top` / `bottom` names y and centres x, any other lone value is x
|
|
212
|
+
* with y centred, and a pair is x then y unless it leads with `top` /
|
|
213
|
+
* `bottom` (or with `center` before `left` / `right`). Three- and four-value
|
|
214
|
+
* edge offsets keep their edge keywords only.
|
|
215
|
+
*/
|
|
216
|
+
function splitBackgroundPosition(raw) {
|
|
217
|
+
const parts = raw.toLowerCase().split(/\s+/);
|
|
218
|
+
if (parts.length === 1) {
|
|
219
|
+
const [only] = parts;
|
|
220
|
+
return only === "top" || only === "bottom" ? {
|
|
221
|
+
x: "center",
|
|
222
|
+
y: only,
|
|
223
|
+
exact: true
|
|
224
|
+
} : {
|
|
225
|
+
x: only,
|
|
226
|
+
y: "center",
|
|
227
|
+
exact: true
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (parts.length === 2) {
|
|
231
|
+
const [a, b] = parts;
|
|
232
|
+
return a === "top" || a === "bottom" || a === "center" && (b === "left" || b === "right") ? {
|
|
233
|
+
x: b,
|
|
234
|
+
y: a,
|
|
235
|
+
exact: true
|
|
236
|
+
} : {
|
|
237
|
+
x: a,
|
|
238
|
+
y: b,
|
|
239
|
+
exact: true
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
x: parts.find((part) => part === "left" || part === "right") ?? "center",
|
|
244
|
+
y: parts.find((part) => part === "top" || part === "bottom") ?? "center",
|
|
245
|
+
exact: false
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* One axis as a grid point: its keyword; the point nearest a percentage,
|
|
250
|
+
* ties going to the centre; or the start edge for a length, which is an
|
|
251
|
+
* offset from it. `null` when the token is unreadable.
|
|
252
|
+
*/
|
|
253
|
+
function backgroundAxisPoint(token, grid) {
|
|
254
|
+
const keyword = grid.find((point) => point === token);
|
|
255
|
+
if (keyword) return {
|
|
256
|
+
point: keyword,
|
|
257
|
+
exact: true
|
|
258
|
+
};
|
|
259
|
+
const percent = PERCENT.exec(token);
|
|
260
|
+
if (percent) {
|
|
261
|
+
const n = Number(percent[1]);
|
|
262
|
+
return {
|
|
263
|
+
point: n < 25 ? grid[0] : n > 75 ? grid[2] : grid[1],
|
|
264
|
+
exact: n === 0 || n === 50 || n === 100
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (LENGTH.test(token)) return {
|
|
268
|
+
point: grid[0],
|
|
269
|
+
exact: parseFloat(token) === 0
|
|
270
|
+
};
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* `background-repeat`: unrepeated only when every axis is `no-repeat`.
|
|
275
|
+
* Anything but plain `repeat` tiles too, inexactly.
|
|
276
|
+
*/
|
|
277
|
+
function backgroundRepeatFlag(raw) {
|
|
278
|
+
const parts = raw.toLowerCase().split(/\s+/);
|
|
279
|
+
if (parts.every((part) => part === "no-repeat")) return {
|
|
280
|
+
repeat: false,
|
|
281
|
+
exact: true
|
|
282
|
+
};
|
|
283
|
+
return {
|
|
284
|
+
repeat: true,
|
|
285
|
+
exact: parts.every((part) => part === "repeat")
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* An `mj-section` / `mj-wrapper` background image, `null` when
|
|
290
|
+
* `background-url` is absent or blank. `read` returns an attribute's
|
|
291
|
+
* resolved value. Unset attributes take MJML's own defaults, so the import
|
|
292
|
+
* paints what MJML painted; a value with no exact keyword takes the nearest
|
|
293
|
+
* one and adds a note naming it. `label` names the element in each note, and
|
|
294
|
+
* a px `background-size` is measured against `boxWidth`.
|
|
295
|
+
*/
|
|
296
|
+
function readMjmlBackgroundImage(read, label, boxWidth) {
|
|
297
|
+
const url = (read("background-url") ?? "").trim();
|
|
298
|
+
if (!url) return null;
|
|
299
|
+
const notes = [];
|
|
300
|
+
const lossy = (name, raw, shown) => {
|
|
301
|
+
notes.push(`${label} ${name} "${raw}" has no exact Templatical equivalent; imported as "${shown}".`);
|
|
302
|
+
};
|
|
303
|
+
let size = MJML_BACKGROUND_DEFAULTS.size;
|
|
304
|
+
const rawSize = read("background-size")?.trim();
|
|
305
|
+
if (rawSize) {
|
|
306
|
+
const parsed = backgroundSizeKeyword(rawSize, boxWidth);
|
|
307
|
+
size = parsed.size;
|
|
308
|
+
if (!parsed.exact) lossy("background-size", rawSize, size);
|
|
309
|
+
}
|
|
310
|
+
let x = MJML_BACKGROUND_DEFAULTS.x;
|
|
311
|
+
let y = MJML_BACKGROUND_DEFAULTS.y;
|
|
312
|
+
let lossyX = false;
|
|
313
|
+
let lossyY = false;
|
|
314
|
+
const rawPosition = read("background-position")?.trim();
|
|
315
|
+
if (rawPosition) {
|
|
316
|
+
const split = splitBackgroundPosition(rawPosition);
|
|
317
|
+
const readX = backgroundAxisPoint(split.x, X_GRID);
|
|
318
|
+
const readY = backgroundAxisPoint(split.y, Y_GRID);
|
|
319
|
+
x = readX?.point ?? x;
|
|
320
|
+
y = readY?.point ?? y;
|
|
321
|
+
lossyX = !split.exact || !readX?.exact;
|
|
322
|
+
lossyY = !split.exact || !readY?.exact;
|
|
323
|
+
}
|
|
324
|
+
const rawX = read("background-position-x")?.trim();
|
|
325
|
+
const overrideX = rawX ? backgroundAxisPoint(rawX.toLowerCase(), X_GRID) : null;
|
|
326
|
+
if (overrideX) {
|
|
327
|
+
x = overrideX.point;
|
|
328
|
+
lossyX = false;
|
|
329
|
+
}
|
|
330
|
+
const rawY = read("background-position-y")?.trim();
|
|
331
|
+
const overrideY = rawY ? backgroundAxisPoint(rawY.toLowerCase(), Y_GRID) : null;
|
|
332
|
+
if (overrideY) {
|
|
333
|
+
y = overrideY.point;
|
|
334
|
+
lossyY = false;
|
|
335
|
+
}
|
|
336
|
+
if (rawPosition && (lossyX || lossyY)) lossy("background-position", rawPosition, `${x} ${y}`);
|
|
337
|
+
if (rawX && !overrideX?.exact) lossy("background-position-x", rawX, x);
|
|
338
|
+
if (rawY && !overrideY?.exact) lossy("background-position-y", rawY, y);
|
|
339
|
+
let repeat = MJML_BACKGROUND_DEFAULTS.repeat;
|
|
340
|
+
const rawRepeat = read("background-repeat")?.trim();
|
|
341
|
+
if (rawRepeat) {
|
|
342
|
+
const parsed = backgroundRepeatFlag(rawRepeat);
|
|
343
|
+
repeat = parsed.repeat;
|
|
344
|
+
if (!parsed.exact) lossy("background-repeat", rawRepeat, repeat ? "repeat" : "no-repeat");
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
image: {
|
|
348
|
+
url,
|
|
349
|
+
size,
|
|
350
|
+
position: {
|
|
351
|
+
x,
|
|
352
|
+
y
|
|
353
|
+
},
|
|
354
|
+
repeat
|
|
355
|
+
},
|
|
356
|
+
notes
|
|
357
|
+
};
|
|
358
|
+
}
|
|
168
359
|
//#endregion
|
|
169
360
|
//#region src/global-style.ts
|
|
170
361
|
/**
|
|
@@ -415,7 +606,9 @@ function convertImage(node, ctx, sourceTag) {
|
|
|
415
606
|
const widthPercent = numAttr(node, "widthPercent");
|
|
416
607
|
const pxWidth = numAttr(node, "width");
|
|
417
608
|
const href = attr(node, "href");
|
|
418
|
-
const
|
|
609
|
+
const base = baseStyles(node);
|
|
610
|
+
const room = ctx.columnWidth - base.styles.padding.left - base.styles.padding.right;
|
|
611
|
+
const width = widthPercent === 100 || pxWidth === void 0 || pxWidth >= room ? "full" : Math.round(pxWidth);
|
|
419
612
|
const isGif = sourceTag === "mj-gif";
|
|
420
613
|
return {
|
|
421
614
|
block: createImageBlock({
|
|
@@ -424,7 +617,7 @@ function convertImage(node, ctx, sourceTag) {
|
|
|
424
617
|
width,
|
|
425
618
|
align: parseAlignment(attr(node, "align"), "center"),
|
|
426
619
|
...href ? { linkUrl: href } : {},
|
|
427
|
-
...
|
|
620
|
+
...base
|
|
428
621
|
}),
|
|
429
622
|
entry: {
|
|
430
623
|
sourceTag,
|
|
@@ -749,11 +942,13 @@ function buildSection(node, ctx, entries) {
|
|
|
749
942
|
const slots = COLUMN_COUNT[layout];
|
|
750
943
|
const pixels = columnPixels(layout, ctx.style.settings.width);
|
|
751
944
|
const children = Array.from({ length: slots }, () => []);
|
|
945
|
+
const background = readMjmlBackgroundImage((name) => attr(node, name), "Section", ctx.style.settings.width);
|
|
946
|
+
const notes = [...exact ? [] : [`Column widths ${rawWidths.map((w) => w ?? "auto").join(", ")} have no exact Templatical layout; resolved to "${layout}".`], ...background?.notes ?? []];
|
|
752
947
|
entries.push({
|
|
753
948
|
sourceTag: "mj-section",
|
|
754
949
|
templaticalBlockType: "section",
|
|
755
|
-
status:
|
|
756
|
-
...
|
|
950
|
+
status: notes.length > 0 ? "approximated" : "converted",
|
|
951
|
+
...notes.length > 0 ? { note: notes.join(" ") } : {}
|
|
757
952
|
});
|
|
758
953
|
columns.forEach((column, index) => {
|
|
759
954
|
const slot = Math.min(index, slots - 1);
|
|
@@ -770,7 +965,8 @@ function buildSection(node, ctx, entries) {
|
|
|
770
965
|
styles: {
|
|
771
966
|
padding: parsePadding(node),
|
|
772
967
|
...backgroundColor ? { backgroundColor } : {}
|
|
773
|
-
}
|
|
968
|
+
},
|
|
969
|
+
...background ? { backgroundImage: background.image } : {}
|
|
774
970
|
})];
|
|
775
971
|
}
|
|
776
972
|
//#endregion
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/attribute-parser.ts","../src/global-style.ts","../src/content-inference.ts","../src/block-mapper.ts","../src/social-mapper.ts","../src/section-builder.ts","../src/converter.ts"],"sourcesContent":["import type { SpacingValue } from \"@templatical/types\";\nimport type { TopolNode } from \"./types\";\n\n/**\n * Read one attribute as a string, treating Topol's explicit `null` — and an\n * empty string — as absent.\n *\n * Topol writes literal `null` for an unset attribute rather than omitting the\n * key, so a `!== undefined` check would report it as present and write a null\n * into the block. Every optional-field read goes through here.\n */\nexport function attr(node: TopolNode, key: string): string | undefined {\n const raw = node.attributes?.[key];\n if (raw === null || raw === undefined) return undefined;\n if (typeof raw === \"object\") return undefined;\n const value = String(raw);\n return value === \"\" ? undefined : value;\n}\n\n/** Read one attribute as a number, or `undefined` when unset or non-numeric. */\nexport function numAttr(node: TopolNode, key: string): number | undefined {\n const value = attr(node, key);\n if (value === undefined) return undefined;\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\n/**\n * Parse a px-like value into a rounded integer. Returns 0 for a missing value\n * and for units the block model cannot express (em, rem, %).\n *\n * Trims first, then matches with no `\\s*` on either side of the optional\n * `px` unit. A trailing run of spaces can otherwise be claimed by either the\n * surrounding `\\s*` or by backtracking out of the digit/unit match, and a\n * *failing* match (no `px`, no digits — anything not shaped like a length)\n * retries every such split before giving up: `\"0\" + \" \".repeat(n) + \"x\"` is\n * polynomial in `n` against that pattern. Trimming first removes the only\n * whitespace the pattern needs to tolerate, so the match itself has nothing\n * ambiguous left to backtrack over.\n */\nexport function parsePxValue(value: string | number | undefined): number {\n if (value === undefined || value === null || value === \"\") return 0;\n if (typeof value === \"number\") return Math.round(value);\n const trimmed = value.trim();\n const match = /^(-?\\d+(?:\\.\\d+)?)(?:px)?$/.exec(trimmed);\n return match ? Math.round(parseFloat(match[1])) : 0;\n}\n\nconst NAMED_COLORS: Record<string, string> = {\n black: \"#000000\",\n white: \"#ffffff\",\n red: \"#ff0000\",\n green: \"#008000\",\n blue: \"#0000ff\",\n yellow: \"#ffff00\",\n cyan: \"#00ffff\",\n magenta: \"#ff00ff\",\n gray: \"#808080\",\n grey: \"#808080\",\n silver: \"#c0c0c0\",\n maroon: \"#800000\",\n olive: \"#808000\",\n lime: \"#00ff00\",\n aqua: \"#00ffff\",\n teal: \"#008080\",\n navy: \"#000080\",\n fuchsia: \"#ff00ff\",\n purple: \"#800080\",\n orange: \"#ffa500\",\n pink: \"#ffc0cb\",\n};\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n const clamp = (n: number) => Math.max(0, Math.min(255, Math.round(n)));\n const hex = (n: number) => clamp(n).toString(16).padStart(2, \"0\");\n return `#${hex(r)}${hex(g)}${hex(b)}`;\n}\n\n/**\n * Normalize a colour to 6-digit lowercase hex.\n *\n * Returns `\"\"` for transparent/inherit/none and for anything unrecognised —\n * the empty string is the block model's \"unset\", which the colour pickers\n * clear to, so it is meaningfully different from returning a default.\n */\nexport function parseColor(value: string | undefined): string {\n if (!value) return \"\";\n const trimmed = value.trim().toLowerCase();\n if (trimmed === \"transparent\" || trimmed === \"inherit\" || trimmed === \"none\")\n return \"\";\n if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed;\n if (/^#[0-9a-f]{3}$/.test(trimmed)) {\n const [, r, g, b] = trimmed;\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n const rgb = trimmed.match(\n /^rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(?:,\\s*[\\d.]+\\s*)?\\)$/,\n );\n if (rgb) return rgbToHex(+rgb[1], +rgb[2], +rgb[3]);\n return NAMED_COLORS[trimmed] ?? \"\";\n}\n\nfunction expandShorthand(value: string | undefined): SpacingValue {\n if (!value) return { top: 0, right: 0, bottom: 0, left: 0 };\n const v = value\n .trim()\n .split(/\\s+/)\n .map((p) => parsePxValue(p));\n switch (v.length) {\n case 1:\n return { top: v[0], right: v[0], bottom: v[0], left: v[0] };\n case 2:\n return { top: v[0], right: v[1], bottom: v[0], left: v[1] };\n case 3:\n return { top: v[0], right: v[1], bottom: v[2], left: v[1] };\n default:\n return { top: v[0], right: v[1], bottom: v[2], left: v[3] };\n }\n}\n\n/**\n * Read a node's padding, handling both forms Topol emits: a shorthand string\n * (`\"9px 9px 9px 9px\"`, used by most leaves) and four separate bare-number\n * keys (`padding-top: 24`, used by `mj-divider`). A separate key wins over the\n * shorthand for its own side.\n */\nexport function parsePadding(node: TopolNode): SpacingValue {\n const base = expandShorthand(attr(node, \"padding\"));\n const side = (key: string, fallback: number) => {\n const value = attr(node, key);\n return value === undefined ? fallback : parsePxValue(value);\n };\n return {\n top: side(\"padding-top\", base.top),\n right: side(\"padding-right\", base.right),\n bottom: side(\"padding-bottom\", base.bottom),\n left: side(\"padding-left\", base.left),\n };\n}\n\n/**\n * Read a percentage. Returns `null` when the value is not a percentage, so a\n * caller can tell \"no width given\" from \"0%\" — column matching depends on that\n * distinction.\n */\nexport function parsePercent(\n value: string | number | undefined,\n): number | null {\n if (value === undefined || value === null || value === \"\") return null;\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n const match = value.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*%$/);\n return match ? parseFloat(match[1]) : null;\n}\n\n/** Narrow an alignment to the three the block model accepts. */\nexport function parseAlignment(\n value: string | undefined,\n fallback: \"left\" | \"center\" | \"right\" = \"left\",\n): \"left\" | \"center\" | \"right\" {\n const v = (value ?? \"\").trim().toLowerCase();\n return v === \"left\" || v === \"center\" || v === \"right\" ? v : fallback;\n}\n\n/** Narrow a border style to the three `DividerBlock.lineStyle` accepts. */\nexport function parseBorderStyle(\n value: string | undefined,\n): \"solid\" | \"dashed\" | \"dotted\" {\n const v = (value ?? \"\").trim().toLowerCase();\n return v === \"dashed\" || v === \"dotted\" ? v : \"solid\";\n}\n","import {\n canonicalFontStack,\n DEFAULT_TEMPLATE_DEFAULTS,\n} from \"@templatical/types\";\nimport type { TemplateContent, TemplateSettings } from \"@templatical/types\";\nimport { attr, parseColor, parsePxValue } from \"./attribute-parser\";\nimport type { TopolDesign, TopolNode } from \"./types\";\n\n/**\n * `DEFAULT_TEMPLATE_DEFAULTS` is typed `Partial<TemplateSettings>` so a\n * consumer can override any subset, but its own literal always sets the six\n * required fields. Narrowing once here is what lets each read come out as\n * `number`/`string`/`boolean` rather than `| undefined`. A plain typed\n * assignment cannot substitute: `Partial<T>` is not assignable to\n * `Required<Pick<T, …>>`, so `tsc` rejects it outright.\n */\nconst REQUIRED_DEFAULTS = DEFAULT_TEMPLATE_DEFAULTS as Required<\n Pick<\n TemplateSettings,\n | \"width\"\n | \"backgroundColor\"\n | \"textColor\"\n | \"linkUnderline\"\n | \"fontFamily\"\n | \"locale\"\n >\n>;\n\nexport interface GlobalStyle {\n settings: TemplateContent[\"settings\"];\n /** From nested objects like `\"mj-text\": { … }`. */\n tagDefaults: Record<string, Record<string, string>>;\n /** From flat `<selector>:<property>` keys; the bare selector is `\"\"`. */\n selectorDefaults: Record<string, Record<string, string>>;\n}\n\n/** A per-tag default, or undefined when the design sets none. */\nexport function tagDefault(\n style: GlobalStyle,\n tag: string,\n key: string,\n): string | undefined {\n return style.tagDefaults[tag]?.[key];\n}\n\n/** A per-selector default (`\"\"` is the document default), or undefined. */\nexport function selectorDefault(\n style: GlobalStyle,\n selector: string,\n key: string,\n): string | undefined {\n return style.selectorDefaults[selector]?.[key];\n}\n\n/**\n * Split the root's `attributes` into settings, per-tag defaults and\n * per-selector defaults.\n *\n * The root mixes two idioms: `<selector>:<property>` flat keys (`:color`,\n * `a:color`, `h1:font-family`) and `<tag>: { … }` nested objects, which are\n * the analogue of MJML's `mj-attributes`.\n */\nexport function readGlobalStyle(\n root: TopolDesign,\n container: TopolNode | undefined,\n warnings: string[],\n): GlobalStyle {\n const tagDefaults: Record<string, Record<string, string>> = Object.create(\n null,\n );\n const selectorDefaults: Record<\n string,\n Record<string, string>\n > = Object.create(null);\n\n for (const [key, value] of Object.entries(root.attributes ?? {})) {\n if (value !== null && typeof value === \"object\") {\n const bucket = (tagDefaults[key] ??= Object.create(null));\n for (const [k, v] of Object.entries(value)) {\n if (v !== null && v !== undefined) bucket[k] = String(v);\n }\n continue;\n }\n\n const colon = key.indexOf(\":\");\n // No colon means a root-level setting rather than a selector default —\n // `containerWidth` and `fonts` are the two real cases, and both are read\n // directly by name below instead of through the caches built here.\n if (colon === -1) continue;\n const selector = key.slice(0, colon);\n const property = key.slice(colon + 1);\n if (value === null || value === undefined) continue;\n const bucket = (selectorDefaults[selector] ??= Object.create(null));\n bucket[property] = String(value);\n }\n\n // containerWidth is authored only here, on the root. It is also denormalised\n // onto every leaf as the enclosing column's computed width (600/300/150 in\n // one template), so no other module may read it.\n const width =\n parsePxValue(attr(root, \"containerWidth\")) || REQUIRED_DEFAULTS.width;\n\n const backgroundColor =\n (container ? parseColor(attr(container, \"background-color\")) : \"\") ||\n REQUIRED_DEFAULTS.backgroundColor;\n\n const textColor =\n parseColor(selectorDefaults[\"\"]?.color) || REQUIRED_DEFAULTS.textColor;\n\n const fontFamily =\n canonicalFontStack(selectorDefaults[\"\"]?.[\"font-family\"]) ||\n canonicalFontStack(attr(root, \"fonts\")) ||\n REQUIRED_DEFAULTS.fontFamily;\n\n const linkColor = parseColor(selectorDefaults[\"a\"]?.color);\n\n const lineHeight = selectorDefaults[\"\"]?.[\"line-height\"];\n if (lineHeight !== undefined) {\n warnings.push(\n `Dropped the document line-height (${lineHeight}) — Templatical has no document-level line-height setting.`,\n );\n }\n\n const settings: TemplateContent[\"settings\"] = {\n width,\n backgroundColor,\n textColor,\n linkUnderline: REQUIRED_DEFAULTS.linkUnderline,\n fontFamily,\n locale: REQUIRED_DEFAULTS.locale,\n ...(linkColor ? { linkColor } : {}),\n };\n\n return { settings, tagDefaults, selectorDefaults };\n}\n","import { findOpenTagEnd } from \"@templatical/types\";\nimport type { HeadingLevel } from \"@templatical/types\";\n\nexport type InferredText =\n | { kind: \"title\"; level: HeadingLevel; inner: string; clampedFrom?: number }\n | { kind: \"paragraph\"; html: string };\n\n/**\n * A single heading element wrapping the whole content, captured with its level\n * and inner markup. Anchored at both ends so a heading with a sibling does not\n * match — that case is a paragraph.\n */\nconst SOLE_HEADING = /^<h([1-6])(?:\\s[^>]*)?>([\\s\\S]*)<\\/h\\1>$/i;\n\nconst BLOCK_ROOT = /^<(p|h[1-6]|ul|ol|blockquote|div|table)\\b/i;\n\n/**\n * Resolve a Topol `content` string to a Title or a Paragraph.\n *\n * Topol has no menu or table construct, so this is a two-way choice — the MJML\n * importer needs four arms because `mj-text` is the output of four different\n * block renderers there. Paragraph is the terminal arm and always reachable,\n * so this function is total.\n */\nexport function inferTextBlock(content: string | undefined): InferredText {\n const trimmed = (content ?? \"\").trim();\n if (trimmed === \"\") return { kind: \"paragraph\", html: \"<p></p>\" };\n\n const heading = trimmed.match(SOLE_HEADING);\n if (heading) {\n const source = Number(heading[1]);\n const level = Math.min(source, 4) as HeadingLevel;\n return {\n kind: \"title\",\n level,\n inner: heading[2].trim(),\n ...(source > 4 ? { clampedFrom: source } : {}),\n };\n }\n\n return {\n kind: \"paragraph\",\n html: BLOCK_ROOT.test(trimmed) ? trimmed : `<p>${trimmed}</p>`,\n };\n}\n\nconst ENTITIES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n \""\": '\"',\n \"'\": \"'\",\n \"'\": \"'\",\n \" \": \" \",\n};\n\n/**\n * Remove every `<tag>` from `html`, keeping the text between them.\n *\n * A single-pass linear scan rather than `/<[^>]*>/g`: that regex is\n * polynomial over input with many `<` starts and no closing `>`, since\n * `[^>]*` backtracks at every one of them — `\"<\".repeat(n)` is quadratic in\n * `n`. `findOpenTagEnd` (from `@templatical/types`) resolves each `<` with a\n * bounded, quote-aware forward scan instead, so the walk stays O(n): every\n * character is visited at most once by `indexOf` and once by\n * `findOpenTagEnd`, because the cursor only moves forward and an\n * unterminated tag ends the walk rather than retrying from inside it. On\n * that unterminated case the remainder of the string — the dangling `<`\n * included — is emitted as-is, matching what `/<[^>]*>/g` does with\n * malformed markup: it can never complete a match without a `>`, so the\n * text is left untouched from that point on.\n */\nfunction removeTags(html: string): string {\n let out = \"\";\n let i = 0;\n while (i < html.length) {\n const open = html.indexOf(\"<\", i);\n if (open === -1) {\n out += html.substring(i);\n break;\n }\n out += html.substring(i, open);\n const tagEnd = findOpenTagEnd(html, open + 1);\n if (tagEnd === -1) {\n out += html.substring(open);\n break;\n }\n i = tagEnd + 1;\n }\n return out;\n}\n\n/**\n * The visible text of a markup fragment, for a button label — `ButtonBlock.text`\n * is a plain string, so the markup Topol stores in `content` cannot travel.\n */\nexport function stripTags(html: string | undefined): string {\n if (!html) return \"\";\n const text = removeTags(html);\n const decoded = text.replace(\n /&(?:amp|lt|gt|quot|#39|apos|nbsp);/g,\n (m) => ENTITIES[m] ?? m,\n );\n return decoded.replace(/\\s+/g, \" \").trim();\n}\n","import {\n canonicalFontStack,\n createButtonBlock,\n createDividerBlock,\n createHtmlBlock,\n createImageBlock,\n createParagraphBlock,\n createSpacerBlock,\n createTitleBlock,\n} from \"@templatical/types\";\nimport type { Block, BlockStyles } from \"@templatical/types\";\nimport {\n attr,\n numAttr,\n parseAlignment,\n parseBorderStyle,\n parseColor,\n parsePadding,\n parsePxValue,\n} from \"./attribute-parser\";\nimport { inferTextBlock, stripTags } from \"./content-inference\";\nimport { selectorDefault, tagDefault, type GlobalStyle } from \"./global-style\";\nimport type { ImportReportEntry, TopolNode } from \"./types\";\n\nexport interface MapContext {\n style: GlobalStyle;\n /** The rendered width of the column holding this node, for `width: \"full\"`. */\n columnWidth: number;\n}\n\nexport interface Converted {\n block: Block | null;\n entry: ImportReportEntry;\n}\n\n/**\n * Read a node's own attribute, falling back to the design's per-tag then\n * per-selector default. Returns undefined when nothing sets it, so the caller\n * can omit the field and let the block factory's default stand.\n */\nfunction resolved(\n node: TopolNode,\n key: string,\n ctx: MapContext,\n selector?: string,\n): string | undefined {\n return (\n attr(node, key) ??\n tagDefault(ctx.style, node.tagName, key) ??\n (selector === undefined\n ? undefined\n : selectorDefault(ctx.style, selector, key))\n );\n}\n\n/**\n * The `styles` every leaf block shares.\n *\n * The block background is the row behind the element, which MJML reads from\n * `container-background-color` on every leaf and which the renderer writes\n * `styles.backgroundColor` back out as. A leaf's `background-color` must not\n * feed it: on `mj-button` it is the button face (`backgroundColor`, read in\n * `convertButton`), on `mj-text` MJML never paints it, and on image, spacer,\n * divider and social it is invalid and dropped. Reading it here paints a\n * button's whole row in its fill colour. `buildSection` reads\n * `background-color` itself, since on `mj-section` it is the section's fill.\n */\nexport function baseStyles(node: TopolNode): { styles: BlockStyles } {\n const backgroundColor = parseColor(attr(node, \"container-background-color\"));\n return {\n styles: {\n padding: parsePadding(node),\n ...(backgroundColor ? { backgroundColor } : {}),\n },\n };\n}\n\nfunction convertText(node: TopolNode, ctx: MapContext): Converted {\n const inferred = inferTextBlock(node.content);\n\n if (inferred.kind === \"paragraph\") {\n return {\n block: createParagraphBlock({\n content: inferred.html,\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n }\n\n const selector = `h${inferred.level}`;\n const color = parseColor(resolved(node, \"color\", ctx, selector));\n const fontFamily = canonicalFontStack(\n resolved(node, \"font-family\", ctx, selector),\n );\n\n const block = createTitleBlock({\n content: inferred.inner,\n level: inferred.level,\n textAlign: parseAlignment(attr(node, \"align\"), \"left\"),\n ...(color ? { color } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseStyles(node),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"title\",\n status: inferred.clampedFrom ? \"approximated\" : \"converted\",\n ...(inferred.clampedFrom\n ? {\n note: `Heading level h${inferred.clampedFrom} clamped to 4 — Templatical titles support h1-h4.`,\n }\n : {}),\n },\n };\n}\n\nfunction convertButton(node: TopolNode, ctx: MapContext): Converted | null {\n const text = stripTags(node.content);\n if (!text) return null;\n\n const backgroundColor = parseColor(\n resolved(node, \"background-color\", ctx, \"button\"),\n );\n const textColor = parseColor(resolved(node, \"color\", ctx, \"button\"));\n const fontFamily = canonicalFontStack(\n resolved(node, \"font-family\", ctx, \"button\"),\n );\n const fontSizeRaw = resolved(node, \"font-size\", ctx);\n const radiusRaw = resolved(node, \"border-radius\", ctx);\n\n return {\n block: createButtonBlock({\n text,\n url: attr(node, \"href\") ?? \"\",\n ...(backgroundColor ? { backgroundColor } : {}),\n ...(textColor ? { textColor } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...(fontSizeRaw === undefined\n ? {}\n : { fontSize: parsePxValue(fontSizeRaw) }),\n ...(radiusRaw === undefined\n ? {}\n : { borderRadius: parsePxValue(radiusRaw) }),\n align: parseAlignment(attr(node, \"align\"), \"center\"),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: node.tagName,\n templaticalBlockType: \"button\",\n status: \"converted\",\n },\n };\n}\n\nfunction convertImage(\n node: TopolNode,\n ctx: MapContext,\n sourceTag: string,\n): Converted | null {\n const src = attr(node, \"src\");\n if (!src) return null;\n\n const widthPercent = numAttr(node, \"widthPercent\");\n const pxWidth = numAttr(node, \"width\");\n const href = attr(node, \"href\");\n\n const width =\n widthPercent === 100 || pxWidth === undefined || pxWidth >= ctx.columnWidth\n ? (\"full\" as const)\n : Math.round(pxWidth);\n\n const isGif = sourceTag === \"mj-gif\";\n\n return {\n block: createImageBlock({\n src,\n alt: attr(node, \"alt\") ?? \"\",\n width,\n align: parseAlignment(attr(node, \"align\"), \"center\"),\n ...(href ? { linkUrl: href } : {}),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag,\n templaticalBlockType: \"image\",\n status: isGif ? \"approximated\" : \"converted\",\n ...(isGif\n ? {\n note: \"<mj-gif> imported as an image block; Templatical has no dedicated GIF block.\",\n }\n : {}),\n },\n };\n}\n\nfunction convertSpacer(node: TopolNode): Converted {\n const height = attr(node, \"height\");\n return {\n block: createSpacerBlock({\n ...(height === undefined ? {} : { height: parsePxValue(height) }),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-spacer\",\n templaticalBlockType: \"spacer\",\n status: \"converted\",\n },\n };\n}\n\nfunction convertDivider(node: TopolNode): Converted {\n const color = parseColor(attr(node, \"border-color\"));\n const thickness = attr(node, \"border-width\");\n return {\n block: createDividerBlock({\n lineStyle: parseBorderStyle(attr(node, \"border-style\")),\n ...(color ? { color } : {}),\n ...(thickness === undefined\n ? {}\n : { thickness: parsePxValue(thickness) }),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-divider\",\n templaticalBlockType: \"divider\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * Convert one leaf node to a block.\n *\n * Returns `null` for a node that produces nothing *and* warrants no report\n * entry — an image with no `src`, a button with no label. `mj-social` is not\n * handled here; the converter routes it to `convertSocial`.\n */\nexport function convertLeaf(\n node: TopolNode,\n ctx: MapContext,\n): Converted | null {\n switch (node.tagName) {\n case \"mj-text\":\n return convertText(node, ctx);\n case \"mj-button\":\n return convertButton(node, ctx);\n case \"mj-image\":\n return convertImage(node, ctx, \"mj-image\");\n case \"mj-gif\":\n return convertImage(node, ctx, \"mj-gif\");\n case \"mj-spacer\":\n return convertSpacer(node);\n case \"mj-divider\":\n return convertDivider(node);\n default:\n return {\n block: createHtmlBlock({\n content: JSON.stringify(node, null, 2),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: node.tagName,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `<${node.tagName}> has no Templatical block equivalent; the original node is preserved as JSON.`,\n },\n };\n }\n}\n","import { createSocialIconsBlock, generateId } from \"@templatical/types\";\nimport type {\n SocialIcon,\n SocialIconTone,\n SocialIconSize,\n SocialIconStyle,\n SocialPlatform,\n} from \"@templatical/types\";\nimport { attr, parseAlignment, parsePxValue } from \"./attribute-parser\";\nimport { baseStyles, type Converted, type MapContext } from \"./block-mapper\";\nimport type { TopolNode } from \"./types\";\n\n/**\n * Exhaustive over `SocialPlatform` on purpose: adding a member to that union\n * without adding it here is a compile error, so this cannot silently fall back\n * to \"website\" for a platform the block model gained.\n */\nconst KNOWN_PLATFORMS: Record<SocialPlatform, true> = {\n facebook: true,\n twitter: true,\n instagram: true,\n linkedin: true,\n youtube: true,\n tiktok: true,\n pinterest: true,\n email: true,\n whatsapp: true,\n telegram: true,\n discord: true,\n snapchat: true,\n reddit: true,\n github: true,\n dribbble: true,\n behance: true,\n website: true,\n};\n\nconst ALIASES: Record<string, SocialPlatform> = {\n x: \"twitter\",\n \"x-twitter\": \"twitter\",\n};\n\nconst ICON_SIZES: Array<[number, SocialIconSize]> = [\n [24, \"small\"],\n [32, \"medium\"],\n [48, \"large\"],\n];\n\nconst KNOWN_STYLES = new Set<string>([\n \"solid\",\n \"outlined\",\n \"rounded\",\n \"square\",\n \"circle\",\n]);\n\n// Topol ships icon sets whose folder name is not a Templatical style name.\n// `outlinedbw` is the outlined set in black and white, so it imports as\n// outlined in the dark tone; mapping it to the `solid` fallback would import\n// an outlined icon set as filled. Measured: both `/social-icos/outlined/` and\n// `/social-icos/outlinedbw/` appear across the reference exports.\nconst STYLE_ALIASES: Record<\n string,\n { style: SocialIconStyle; tone: SocialIconTone }\n> = {\n outlinedbw: { style: \"outlined\", tone: \"dark\" },\n};\n\nfunction normalizePlatform(raw: string): SocialPlatform | null {\n const cleaned = raw.trim().toLowerCase();\n if (!cleaned) return null;\n if (ALIASES[cleaned]) return ALIASES[cleaned];\n return cleaned in KNOWN_PLATFORMS ? (cleaned as SocialPlatform) : null;\n}\n\nfunction nearestSize(px: number): { size: SocialIconSize; exact: boolean } {\n let best = ICON_SIZES[1];\n let gap = Infinity;\n for (const candidate of ICON_SIZES) {\n const d = Math.abs(candidate[0] - px);\n if (d < gap) {\n gap = d;\n best = candidate;\n }\n }\n return { size: best[1], exact: gap === 0 };\n}\n\n/** The style (and tone) the segment of `https://…/social-icos/<style>/` names. */\nfunction styleFromBaseUrl(\n baseUrl: string | undefined,\n): { style: SocialIconStyle; tone?: SocialIconTone } | undefined {\n if (!baseUrl) return undefined;\n const segment = baseUrl.split(\"?\")[0].split(\"/\").filter(Boolean).at(-1) ?? \"\";\n if (KNOWN_STYLES.has(segment)) return { style: segment as SocialIconStyle };\n return STYLE_ALIASES[segment];\n}\n\n/**\n * Convert Topol's MJML v3 social block.\n *\n * `display` decides which icons render and in what order; a platform with\n * `*-href` set but absent from `display` was not shown in the email and is\n * not imported.\n */\nexport function convertSocial(\n node: TopolNode,\n _ctx: MapContext,\n): Converted | null {\n const display = attr(node, \"display\");\n if (!display) return null;\n\n const notes: string[] = [];\n const icons: SocialIcon[] = [];\n\n for (const token of display.trim().split(/\\s+/)) {\n const rawName = token.split(\":\")[0];\n if (!rawName) continue;\n\n const url = attr(node, `${rawName}-href`);\n if (!url) continue;\n\n const platform = normalizePlatform(rawName);\n if (!platform) {\n notes.push(\n `Unrecognised social platform \"${rawName}\" mapped to \"website\".`,\n );\n }\n\n icons.push({ id: generateId(), platform: platform ?? \"website\", url });\n }\n\n if (icons.length === 0) return null;\n\n const fromBaseUrl = styleFromBaseUrl(attr(node, \"base-url\"));\n\n const declaredSize = attr(node, \"icon-size\");\n let iconSize: SocialIconSize | undefined;\n if (declaredSize !== undefined) {\n const px = parsePxValue(declaredSize);\n const resolved = nearestSize(px);\n iconSize = resolved.size;\n if (!resolved.exact) {\n notes.push(\n `Icon size ${declaredSize} is not one of 24/32/48px; resolved to \"${resolved.size}\".`,\n );\n }\n }\n\n return {\n block: createSocialIconsBlock({\n icons,\n align: parseAlignment(attr(node, \"align\"), \"center\"),\n ...(iconSize ? { iconSize } : {}),\n ...(fromBaseUrl ? { iconStyle: fromBaseUrl.style } : {}),\n ...(fromBaseUrl?.tone ? { iconTone: fromBaseUrl.tone } : {}),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-social\",\n templaticalBlockType: \"social\",\n status: notes.length > 0 ? \"approximated\" : \"converted\",\n ...(notes.length > 0 ? { note: notes.join(\" \") } : {}),\n },\n };\n}\n","import { createSectionBlock } from \"@templatical/types\";\nimport type { Block, ColumnLayout } from \"@templatical/types\";\nimport {\n attr,\n parseColor,\n parsePadding,\n parsePercent,\n} from \"./attribute-parser\";\nimport { convertLeaf, type MapContext } from \"./block-mapper\";\nimport { convertSocial } from \"./social-mapper\";\nimport type { ImportReportEntry, TopolNode } from \"./types\";\n\nconst LAYOUT_SHAPES: Array<{ layout: ColumnLayout; percents: number[] }> = [\n { layout: \"1\", percents: [100] },\n { layout: \"2\", percents: [50, 50] },\n { layout: \"1-2\", percents: [33.33, 66.67] },\n { layout: \"2-1\", percents: [66.67, 33.33] },\n { layout: \"3\", percents: [33.33, 33.33, 33.34] },\n];\n\nconst WIDTH_TOLERANCE = 2;\n\nconst COLUMN_COUNT: Record<ColumnLayout, number> = {\n \"1\": 1,\n \"2\": 2,\n \"3\": 3,\n \"2-1\": 2,\n \"1-2\": 2,\n};\n\nfunction columnPixels(layout: ColumnLayout, container: number): number[] {\n switch (layout) {\n case \"2\":\n return [container / 2, container / 2];\n case \"3\":\n return [container / 3, container / 3, container / 3];\n case \"1-2\":\n return [container / 3, (container * 2) / 3];\n case \"2-1\":\n return [(container * 2) / 3, container / 3];\n default:\n return [container];\n }\n}\n\n/**\n * Resolve column widths to one of the five layouts `ColumnLayout` allows.\n *\n * `exact: false` means the caller reports `approximated` — Topol permits any\n * column count at any width and this union permits five shapes, so this is the\n * importer's main irreducible loss.\n */\nexport function matchColumnLayout(percents: Array<number | null>): {\n layout: ColumnLayout;\n exact: boolean;\n} {\n const count = percents.length;\n if (count === 0) return { layout: \"1\", exact: true };\n\n // All-unset is \"distribute equally\", which is exactly what the n-column\n // layout means — not a missing value to approximate around.\n if (percents.every((p) => p === null)) {\n if (count === 1) return { layout: \"1\", exact: true };\n if (count === 2) return { layout: \"2\", exact: true };\n if (count === 3) return { layout: \"3\", exact: true };\n return { layout: \"3\", exact: false };\n }\n\n const known = percents.filter((p): p is number => p !== null);\n const unsetCount = count - known.length;\n const remainder =\n unsetCount === 0\n ? 0\n : Math.max(0, 100 - known.reduce((a, b) => a + b, 0)) / unsetCount;\n const resolved = percents.map((p) => p ?? remainder);\n\n const sameCount = LAYOUT_SHAPES.filter((s) => s.percents.length === count);\n\n for (const shape of sameCount) {\n if (\n shape.percents.every(\n (want, i) => Math.abs(want - resolved[i]) <= WIDTH_TOLERANCE,\n )\n ) {\n return { layout: shape.layout, exact: true };\n }\n }\n\n const candidates =\n sameCount.length > 0\n ? sameCount\n : LAYOUT_SHAPES.filter((s) => s.layout === \"3\");\n\n let best = candidates[0];\n let bestError = Infinity;\n for (const shape of candidates) {\n const error = shape.percents.reduce(\n (sum, want, i) => sum + Math.abs(want - (resolved[i] ?? 0)),\n 0,\n );\n if (error < bestError) {\n bestError = error;\n best = shape;\n }\n }\n\n return { layout: best.layout, exact: false };\n}\n\nfunction convertColumnChildren(\n column: TopolNode,\n ctx: MapContext,\n entries: ImportReportEntry[],\n): Block[] {\n const blocks: Block[] = [];\n for (const child of column.children ?? []) {\n const converted =\n child.tagName === \"mj-social\"\n ? convertSocial(child, ctx)\n : convertLeaf(child, ctx);\n if (!converted) continue;\n entries.push(converted.entry);\n if (converted.block) blocks.push(converted.block);\n }\n return blocks;\n}\n\n/**\n * Build a `SectionBlock` from an `mj-section`. Returns an array so the caller\n * can treat an empty section (no columns) as producing nothing.\n *\n * Reads `attributes` only. The node's top-level camelCase keys\n * (`backgroundColor`, `paddingTop`, …) are stale editor state that disagrees\n * with `attributes` in every measured case.\n */\nexport function buildSection(\n node: TopolNode,\n ctx: MapContext,\n entries: ImportReportEntry[],\n): Block[] {\n const columns = (node.children ?? []).filter(\n (c) => c.tagName === \"mj-column\",\n );\n if (columns.length === 0) return [];\n\n const rawWidths = columns.map((c) => attr(c, \"width\"));\n const percents = rawWidths.map((w) => parsePercent(w));\n const { layout, exact } = matchColumnLayout(percents);\n\n const slots = COLUMN_COUNT[layout];\n const pixels = columnPixels(layout, ctx.style.settings.width);\n const children: Block[][] = Array.from({ length: slots }, () => []);\n\n entries.push({\n sourceTag: \"mj-section\",\n templaticalBlockType: \"section\",\n status: exact ? \"converted\" : \"approximated\",\n ...(exact\n ? {}\n : {\n note: `Column widths ${rawWidths.map((w) => w ?? \"auto\").join(\", \")} have no exact Templatical layout; resolved to \"${layout}\".`,\n }),\n });\n\n columns.forEach((column, index) => {\n const slot = Math.min(index, slots - 1);\n const columnCtx: MapContext = {\n ...ctx,\n columnWidth: Math.round(pixels[slot] ?? ctx.style.settings.width),\n };\n children[slot].push(...convertColumnChildren(column, columnCtx, entries));\n });\n\n const backgroundColor = parseColor(attr(node, \"background-color\"));\n\n return [\n createSectionBlock({\n columns: layout,\n children,\n styles: {\n padding: parsePadding(node),\n ...(backgroundColor ? { backgroundColor } : {}),\n },\n }),\n ];\n}\n","import { createDefaultTemplateContent } from \"@templatical/types\";\nimport type { Block, TemplateContent } from \"@templatical/types\";\nimport { readGlobalStyle } from \"./global-style\";\nimport { buildSection } from \"./section-builder\";\nimport type { MapContext } from \"./block-mapper\";\nimport type {\n ImportReport,\n ImportReportEntry,\n ImportResult,\n TopolDesign,\n} from \"./types\";\n\nconst ROOT_TAG = \"mj-global-style\";\n\nconst INVALID_DESIGN_MESSAGE =\n 'Invalid Topol template: expected the design JSON object — Topol hands you this directly from its editor\\'s onSave callback, or under \"definition\" (template API) or \"json\" (predefined-templates API) in its API response.';\n\nconst EMPTY_DESIGN_WARNING =\n \"No convertible content was found in the Topol design. Check that the mj-container holds at least one mj-section.\";\n\n/**\n * Convert a Topol.io design into a Templatical template.\n *\n * The input is the design object itself — the node whose `tagName` is\n * `mj-global-style`, not a whole API response. Topol's editor hands it to\n * you directly as the first argument to `onSave`; its REST APIs wrap it\n * instead, under `definition` on the template-retrieval endpoint or `json`\n * (alongside `html`) on the predefined-templates endpoint. That is\n * documented rather than sniffed: guessing which field holds the design\n * risks importing an envelope's `html`, which belongs to a different\n * package entirely.\n *\n * @example\n * ```ts\n * const { content, report } = convertTopolTemplate(design);\n * const editor = init({ container: '#editor', content });\n * console.log(report.summary);\n * ```\n */\nexport function convertTopolTemplate(\n design: TopolDesign | string,\n): ImportResult {\n const root: unknown = typeof design === \"string\" ? safeParse(design) : design;\n\n if (typeof root !== \"object\" || root === null || Array.isArray(root)) {\n throw new Error(INVALID_DESIGN_MESSAGE);\n }\n\n const topolDesign = root as TopolDesign;\n\n if (topolDesign.tagName !== ROOT_TAG) {\n // Topol's REST APIs wrap the design rather than returning it bare: the\n // template-retrieval endpoint uses \"definition\", the predefined/premade\n // templates endpoint uses \"json\" (alongside \"html\"). A response object\n // passed here by mistake still carries one of those keys, so that\n // mistake is named directly instead of falling through to the generic\n // wrong-tagName message below.\n if (\"json\" in topolDesign || \"definition\" in topolDesign) {\n throw new Error(INVALID_DESIGN_MESSAGE);\n }\n throw new Error(\n `Invalid Topol template: expected a root node with tagName \"${ROOT_TAG}\".`,\n );\n }\n\n const container = (topolDesign.children ?? []).find(\n (child) => child.tagName === \"mj-container\",\n );\n\n const entries: ImportReportEntry[] = [];\n const warnings: string[] = [];\n const blocks: Block[] = [];\n\n const style = readGlobalStyle(topolDesign, container, warnings);\n\n const ctx: MapContext = {\n style,\n columnWidth: style.settings.width,\n };\n\n for (const child of container?.children ?? []) {\n if (child.tagName !== \"mj-section\") continue;\n blocks.push(...buildSection(child, ctx, entries));\n }\n\n if (blocks.length === 0) {\n warnings.push(EMPTY_DESIGN_WARNING);\n }\n\n const content: TemplateContent = {\n ...createDefaultTemplateContent(),\n blocks,\n settings: style.settings,\n };\n\n const summary = {\n total: entries.length,\n converted: entries.filter((e) => e.status === \"converted\").length,\n approximated: entries.filter((e) => e.status === \"approximated\").length,\n htmlFallback: entries.filter((e) => e.status === \"html-fallback\").length,\n skipped: entries.filter((e) => e.status === \"skipped\").length,\n };\n\n const report: ImportReport = { entries, warnings, summary };\n\n return { content, report };\n}\n\nfunction safeParse(source: string): unknown {\n try {\n return JSON.parse(source);\n } catch {\n throw new Error(INVALID_DESIGN_MESSAGE);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/attribute-parser.ts","../src/global-style.ts","../src/content-inference.ts","../src/block-mapper.ts","../src/social-mapper.ts","../src/section-builder.ts","../src/converter.ts"],"sourcesContent":["import type {\n BackgroundImage,\n BackgroundPositionX,\n BackgroundPositionY,\n BackgroundSize,\n SpacingValue,\n} from \"@templatical/types\";\nimport type { TopolNode } from \"./types\";\n\n/**\n * Read one attribute as a string, treating Topol's explicit `null` — and an\n * empty string — as absent.\n *\n * Topol writes literal `null` for an unset attribute rather than omitting the\n * key, so a `!== undefined` check would report it as present and write a null\n * into the block. Every optional-field read goes through here.\n */\nexport function attr(node: TopolNode, key: string): string | undefined {\n const raw = node.attributes?.[key];\n if (raw === null || raw === undefined) return undefined;\n if (typeof raw === \"object\") return undefined;\n const value = String(raw);\n return value === \"\" ? undefined : value;\n}\n\n/** Read one attribute as a number, or `undefined` when unset or non-numeric. */\nexport function numAttr(node: TopolNode, key: string): number | undefined {\n const value = attr(node, key);\n if (value === undefined) return undefined;\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\n/**\n * Parse a px-like value into a rounded integer. Returns 0 for a missing value\n * and for units the block model cannot express (em, rem, %).\n *\n * Trims first, then matches with no `\\s*` on either side of the optional\n * `px` unit. A trailing run of spaces can otherwise be claimed by either the\n * surrounding `\\s*` or by backtracking out of the digit/unit match, and a\n * *failing* match (no `px`, no digits — anything not shaped like a length)\n * retries every such split before giving up: `\"0\" + \" \".repeat(n) + \"x\"` is\n * polynomial in `n` against that pattern. Trimming first removes the only\n * whitespace the pattern needs to tolerate, so the match itself has nothing\n * ambiguous left to backtrack over.\n */\nexport function parsePxValue(value: string | number | undefined): number {\n if (value === undefined || value === null || value === \"\") return 0;\n if (typeof value === \"number\") return Math.round(value);\n const trimmed = value.trim();\n const match = /^(-?\\d+(?:\\.\\d+)?)(?:px)?$/.exec(trimmed);\n return match ? Math.round(parseFloat(match[1])) : 0;\n}\n\nconst NAMED_COLORS: Record<string, string> = {\n black: \"#000000\",\n white: \"#ffffff\",\n red: \"#ff0000\",\n green: \"#008000\",\n blue: \"#0000ff\",\n yellow: \"#ffff00\",\n cyan: \"#00ffff\",\n magenta: \"#ff00ff\",\n gray: \"#808080\",\n grey: \"#808080\",\n silver: \"#c0c0c0\",\n maroon: \"#800000\",\n olive: \"#808000\",\n lime: \"#00ff00\",\n aqua: \"#00ffff\",\n teal: \"#008080\",\n navy: \"#000080\",\n fuchsia: \"#ff00ff\",\n purple: \"#800080\",\n orange: \"#ffa500\",\n pink: \"#ffc0cb\",\n};\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n const clamp = (n: number) => Math.max(0, Math.min(255, Math.round(n)));\n const hex = (n: number) => clamp(n).toString(16).padStart(2, \"0\");\n return `#${hex(r)}${hex(g)}${hex(b)}`;\n}\n\n/**\n * Normalize a colour to 6-digit lowercase hex.\n *\n * Returns `\"\"` for transparent/inherit/none and for anything unrecognised —\n * the empty string is the block model's \"unset\", which the colour pickers\n * clear to, so it is meaningfully different from returning a default.\n */\nexport function parseColor(value: string | undefined): string {\n if (!value) return \"\";\n const trimmed = value.trim().toLowerCase();\n if (trimmed === \"transparent\" || trimmed === \"inherit\" || trimmed === \"none\")\n return \"\";\n if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed;\n if (/^#[0-9a-f]{3}$/.test(trimmed)) {\n const [, r, g, b] = trimmed;\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n const rgb = trimmed.match(\n /^rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(?:,\\s*[\\d.]+\\s*)?\\)$/,\n );\n if (rgb) return rgbToHex(+rgb[1], +rgb[2], +rgb[3]);\n return NAMED_COLORS[trimmed] ?? \"\";\n}\n\nfunction expandShorthand(value: string | undefined): SpacingValue {\n if (!value) return { top: 0, right: 0, bottom: 0, left: 0 };\n const v = value\n .trim()\n .split(/\\s+/)\n .map((p) => parsePxValue(p));\n switch (v.length) {\n case 1:\n return { top: v[0], right: v[0], bottom: v[0], left: v[0] };\n case 2:\n return { top: v[0], right: v[1], bottom: v[0], left: v[1] };\n case 3:\n return { top: v[0], right: v[1], bottom: v[2], left: v[1] };\n default:\n return { top: v[0], right: v[1], bottom: v[2], left: v[3] };\n }\n}\n\n/**\n * Read a node's padding, handling both forms Topol emits: a shorthand string\n * (`\"9px 9px 9px 9px\"`, used by most leaves) and four separate bare-number\n * keys (`padding-top: 24`, used by `mj-divider`). A separate key wins over the\n * shorthand for its own side.\n */\nexport function parsePadding(node: TopolNode): SpacingValue {\n const base = expandShorthand(attr(node, \"padding\"));\n const side = (key: string, fallback: number) => {\n const value = attr(node, key);\n return value === undefined ? fallback : parsePxValue(value);\n };\n return {\n top: side(\"padding-top\", base.top),\n right: side(\"padding-right\", base.right),\n bottom: side(\"padding-bottom\", base.bottom),\n left: side(\"padding-left\", base.left),\n };\n}\n\n/**\n * Read a percentage. Returns `null` when the value is not a percentage, so a\n * caller can tell \"no width given\" from \"0%\" — column matching depends on that\n * distinction.\n */\nexport function parsePercent(\n value: string | number | undefined,\n): number | null {\n if (value === undefined || value === null || value === \"\") return null;\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n const match = value.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*%$/);\n return match ? parseFloat(match[1]) : null;\n}\n\n/** Narrow an alignment to the three the block model accepts. */\nexport function parseAlignment(\n value: string | undefined,\n fallback: \"left\" | \"center\" | \"right\" = \"left\",\n): \"left\" | \"center\" | \"right\" {\n const v = (value ?? \"\").trim().toLowerCase();\n return v === \"left\" || v === \"center\" || v === \"right\" ? v : fallback;\n}\n\n/** Narrow a border style to the three `DividerBlock.lineStyle` accepts. */\nexport function parseBorderStyle(\n value: string | undefined,\n): \"solid\" | \"dashed\" | \"dotted\" {\n const v = (value ?? \"\").trim().toLowerCase();\n return v === \"dashed\" || v === \"dotted\" ? v : \"solid\";\n}\n\n/** A background image read from the source, with a note for each value that had no exact keyword. */\nexport interface ReadBackgroundImage {\n image: BackgroundImage;\n notes: string[];\n}\n\nconst X_GRID = [\"left\", \"center\", \"right\"] as const;\nconst Y_GRID = [\"top\", \"center\", \"bottom\"] as const;\nconst PERCENT = /^(-?(?:\\d+(?:\\.\\d+)?|\\.\\d+))%$/;\nconst LENGTH = /^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)[a-z]*$/;\n\n/** What MJML paints for a `background-*` attribute an `mj-section` / `mj-wrapper` leaves out. */\nconst MJML_BACKGROUND_DEFAULTS = {\n size: \"auto\",\n x: \"center\",\n y: \"top\",\n repeat: true,\n} as const;\n\n/**\n * `background-size` as a keyword. A size with a side at 100%, or a px width\n * spanning the box, fills it as `cover` does; any other length or percentage\n * draws the image at a size of its own, which `auto` is closest to.\n */\nfunction backgroundSizeKeyword(\n raw: string,\n boxWidth: number | undefined,\n): { size: BackgroundSize; exact: boolean } {\n const parts = raw.toLowerCase().split(/\\s+/);\n const [first] = parts;\n if (parts.length === 1 && (first === \"cover\" || first === \"contain\")) {\n return { size: first, exact: true };\n }\n if (parts.every((part) => part === \"auto\")) {\n return { size: \"auto\", exact: true };\n }\n const px = /^(\\d+(?:\\.\\d+)?|\\.\\d+)px$/.exec(first);\n const fills =\n parts.includes(\"100%\") ||\n (px !== null && boxWidth !== undefined && Number(px[1]) >= boxWidth);\n return { size: fills ? \"cover\" : \"auto\", exact: false };\n}\n\n/**\n * `background-position` split into x and y the way MJML and CSS read it: a\n * lone `top` / `bottom` names y and centres x, any other lone value is x\n * with y centred, and a pair is x then y unless it leads with `top` /\n * `bottom` (or with `center` before `left` / `right`). Three- and four-value\n * edge offsets keep their edge keywords only.\n */\nfunction splitBackgroundPosition(raw: string): {\n x: string;\n y: string;\n exact: boolean;\n} {\n const parts = raw.toLowerCase().split(/\\s+/);\n if (parts.length === 1) {\n const [only] = parts;\n return only === \"top\" || only === \"bottom\"\n ? { x: \"center\", y: only, exact: true }\n : { x: only, y: \"center\", exact: true };\n }\n if (parts.length === 2) {\n const [a, b] = parts;\n const yFirst =\n a === \"top\" ||\n a === \"bottom\" ||\n (a === \"center\" && (b === \"left\" || b === \"right\"));\n return yFirst ? { x: b, y: a, exact: true } : { x: a, y: b, exact: true };\n }\n return {\n x: parts.find((part) => part === \"left\" || part === \"right\") ?? \"center\",\n y: parts.find((part) => part === \"top\" || part === \"bottom\") ?? \"center\",\n exact: false,\n };\n}\n\n/**\n * One axis as a grid point: its keyword; the point nearest a percentage,\n * ties going to the centre; or the start edge for a length, which is an\n * offset from it. `null` when the token is unreadable.\n */\nfunction backgroundAxisPoint<T extends string>(\n token: string,\n grid: readonly [T, T, T],\n): { point: T; exact: boolean } | null {\n const keyword = grid.find((point) => point === token);\n if (keyword) return { point: keyword, exact: true };\n const percent = PERCENT.exec(token);\n if (percent) {\n const n = Number(percent[1]);\n return {\n point: n < 25 ? grid[0] : n > 75 ? grid[2] : grid[1],\n exact: n === 0 || n === 50 || n === 100,\n };\n }\n if (LENGTH.test(token)) {\n return { point: grid[0], exact: parseFloat(token) === 0 };\n }\n return null;\n}\n\n/**\n * `background-repeat`: unrepeated only when every axis is `no-repeat`.\n * Anything but plain `repeat` tiles too, inexactly.\n */\nfunction backgroundRepeatFlag(raw: string): {\n repeat: boolean;\n exact: boolean;\n} {\n const parts = raw.toLowerCase().split(/\\s+/);\n if (parts.every((part) => part === \"no-repeat\")) {\n return { repeat: false, exact: true };\n }\n return { repeat: true, exact: parts.every((part) => part === \"repeat\") };\n}\n\n/**\n * An `mj-section` / `mj-wrapper` background image, `null` when\n * `background-url` is absent or blank. `read` returns an attribute's\n * resolved value. Unset attributes take MJML's own defaults, so the import\n * paints what MJML painted; a value with no exact keyword takes the nearest\n * one and adds a note naming it. `label` names the element in each note, and\n * a px `background-size` is measured against `boxWidth`.\n */\nexport function readMjmlBackgroundImage(\n read: (name: string) => string | undefined,\n label: string,\n boxWidth: number | undefined,\n): ReadBackgroundImage | null {\n const url = (read(\"background-url\") ?? \"\").trim();\n if (!url) return null;\n\n const notes: string[] = [];\n const lossy = (name: string, raw: string, shown: string): void => {\n notes.push(\n `${label} ${name} \"${raw}\" has no exact Templatical equivalent; imported as \"${shown}\".`,\n );\n };\n\n let size: BackgroundSize = MJML_BACKGROUND_DEFAULTS.size;\n const rawSize = read(\"background-size\")?.trim();\n if (rawSize) {\n const parsed = backgroundSizeKeyword(rawSize, boxWidth);\n size = parsed.size;\n if (!parsed.exact) lossy(\"background-size\", rawSize, size);\n }\n\n let x: BackgroundPositionX = MJML_BACKGROUND_DEFAULTS.x;\n let y: BackgroundPositionY = MJML_BACKGROUND_DEFAULTS.y;\n let lossyX = false;\n let lossyY = false;\n const rawPosition = read(\"background-position\")?.trim();\n if (rawPosition) {\n const split = splitBackgroundPosition(rawPosition);\n const readX = backgroundAxisPoint(split.x, X_GRID);\n const readY = backgroundAxisPoint(split.y, Y_GRID);\n x = readX?.point ?? x;\n y = readY?.point ?? y;\n lossyX = !split.exact || !readX?.exact;\n lossyY = !split.exact || !readY?.exact;\n }\n // A readable `-x` / `-y` replaces its axis, and with it whatever the\n // position lost there; the position's note then names what was imported.\n const rawX = read(\"background-position-x\")?.trim();\n const overrideX = rawX\n ? backgroundAxisPoint(rawX.toLowerCase(), X_GRID)\n : null;\n if (overrideX) {\n x = overrideX.point;\n lossyX = false;\n }\n const rawY = read(\"background-position-y\")?.trim();\n const overrideY = rawY\n ? backgroundAxisPoint(rawY.toLowerCase(), Y_GRID)\n : null;\n if (overrideY) {\n y = overrideY.point;\n lossyY = false;\n }\n if (rawPosition && (lossyX || lossyY)) {\n lossy(\"background-position\", rawPosition, `${x} ${y}`);\n }\n if (rawX && !overrideX?.exact) lossy(\"background-position-x\", rawX, x);\n if (rawY && !overrideY?.exact) lossy(\"background-position-y\", rawY, y);\n\n let repeat: boolean = MJML_BACKGROUND_DEFAULTS.repeat;\n const rawRepeat = read(\"background-repeat\")?.trim();\n if (rawRepeat) {\n const parsed = backgroundRepeatFlag(rawRepeat);\n repeat = parsed.repeat;\n if (!parsed.exact) {\n lossy(\"background-repeat\", rawRepeat, repeat ? \"repeat\" : \"no-repeat\");\n }\n }\n\n return { image: { url, size, position: { x, y }, repeat }, notes };\n}\n","import {\n canonicalFontStack,\n DEFAULT_TEMPLATE_DEFAULTS,\n} from \"@templatical/types\";\nimport type { TemplateContent, TemplateSettings } from \"@templatical/types\";\nimport { attr, parseColor, parsePxValue } from \"./attribute-parser\";\nimport type { TopolDesign, TopolNode } from \"./types\";\n\n/**\n * `DEFAULT_TEMPLATE_DEFAULTS` is typed `Partial<TemplateSettings>` so a\n * consumer can override any subset, but its own literal always sets the six\n * required fields. Narrowing once here is what lets each read come out as\n * `number`/`string`/`boolean` rather than `| undefined`. A plain typed\n * assignment cannot substitute: `Partial<T>` is not assignable to\n * `Required<Pick<T, …>>`, so `tsc` rejects it outright.\n */\nconst REQUIRED_DEFAULTS = DEFAULT_TEMPLATE_DEFAULTS as Required<\n Pick<\n TemplateSettings,\n | \"width\"\n | \"backgroundColor\"\n | \"textColor\"\n | \"linkUnderline\"\n | \"fontFamily\"\n | \"locale\"\n >\n>;\n\nexport interface GlobalStyle {\n settings: TemplateContent[\"settings\"];\n /** From nested objects like `\"mj-text\": { … }`. */\n tagDefaults: Record<string, Record<string, string>>;\n /** From flat `<selector>:<property>` keys; the bare selector is `\"\"`. */\n selectorDefaults: Record<string, Record<string, string>>;\n}\n\n/** A per-tag default, or undefined when the design sets none. */\nexport function tagDefault(\n style: GlobalStyle,\n tag: string,\n key: string,\n): string | undefined {\n return style.tagDefaults[tag]?.[key];\n}\n\n/** A per-selector default (`\"\"` is the document default), or undefined. */\nexport function selectorDefault(\n style: GlobalStyle,\n selector: string,\n key: string,\n): string | undefined {\n return style.selectorDefaults[selector]?.[key];\n}\n\n/**\n * Split the root's `attributes` into settings, per-tag defaults and\n * per-selector defaults.\n *\n * The root mixes two idioms: `<selector>:<property>` flat keys (`:color`,\n * `a:color`, `h1:font-family`) and `<tag>: { … }` nested objects, which are\n * the analogue of MJML's `mj-attributes`.\n */\nexport function readGlobalStyle(\n root: TopolDesign,\n container: TopolNode | undefined,\n warnings: string[],\n): GlobalStyle {\n const tagDefaults: Record<string, Record<string, string>> = Object.create(\n null,\n );\n const selectorDefaults: Record<\n string,\n Record<string, string>\n > = Object.create(null);\n\n for (const [key, value] of Object.entries(root.attributes ?? {})) {\n if (value !== null && typeof value === \"object\") {\n const bucket = (tagDefaults[key] ??= Object.create(null));\n for (const [k, v] of Object.entries(value)) {\n if (v !== null && v !== undefined) bucket[k] = String(v);\n }\n continue;\n }\n\n const colon = key.indexOf(\":\");\n // No colon means a root-level setting rather than a selector default —\n // `containerWidth` and `fonts` are the two real cases, and both are read\n // directly by name below instead of through the caches built here.\n if (colon === -1) continue;\n const selector = key.slice(0, colon);\n const property = key.slice(colon + 1);\n if (value === null || value === undefined) continue;\n const bucket = (selectorDefaults[selector] ??= Object.create(null));\n bucket[property] = String(value);\n }\n\n // containerWidth is authored only here, on the root. It is also denormalised\n // onto every leaf as the enclosing column's computed width (600/300/150 in\n // one template), so no other module may read it.\n const width =\n parsePxValue(attr(root, \"containerWidth\")) || REQUIRED_DEFAULTS.width;\n\n const backgroundColor =\n (container ? parseColor(attr(container, \"background-color\")) : \"\") ||\n REQUIRED_DEFAULTS.backgroundColor;\n\n const textColor =\n parseColor(selectorDefaults[\"\"]?.color) || REQUIRED_DEFAULTS.textColor;\n\n const fontFamily =\n canonicalFontStack(selectorDefaults[\"\"]?.[\"font-family\"]) ||\n canonicalFontStack(attr(root, \"fonts\")) ||\n REQUIRED_DEFAULTS.fontFamily;\n\n const linkColor = parseColor(selectorDefaults[\"a\"]?.color);\n\n const lineHeight = selectorDefaults[\"\"]?.[\"line-height\"];\n if (lineHeight !== undefined) {\n warnings.push(\n `Dropped the document line-height (${lineHeight}) — Templatical has no document-level line-height setting.`,\n );\n }\n\n const settings: TemplateContent[\"settings\"] = {\n width,\n backgroundColor,\n textColor,\n linkUnderline: REQUIRED_DEFAULTS.linkUnderline,\n fontFamily,\n locale: REQUIRED_DEFAULTS.locale,\n ...(linkColor ? { linkColor } : {}),\n };\n\n return { settings, tagDefaults, selectorDefaults };\n}\n","import { findOpenTagEnd } from \"@templatical/types\";\nimport type { HeadingLevel } from \"@templatical/types\";\n\nexport type InferredText =\n | { kind: \"title\"; level: HeadingLevel; inner: string; clampedFrom?: number }\n | { kind: \"paragraph\"; html: string };\n\n/**\n * A single heading element wrapping the whole content, captured with its level\n * and inner markup. Anchored at both ends so a heading with a sibling does not\n * match — that case is a paragraph.\n */\nconst SOLE_HEADING = /^<h([1-6])(?:\\s[^>]*)?>([\\s\\S]*)<\\/h\\1>$/i;\n\nconst BLOCK_ROOT = /^<(p|h[1-6]|ul|ol|blockquote|div|table)\\b/i;\n\n/**\n * Resolve a Topol `content` string to a Title or a Paragraph.\n *\n * Topol has no menu or table construct, so this is a two-way choice — the MJML\n * importer needs four arms because `mj-text` is the output of four different\n * block renderers there. Paragraph is the terminal arm and always reachable,\n * so this function is total.\n */\nexport function inferTextBlock(content: string | undefined): InferredText {\n const trimmed = (content ?? \"\").trim();\n if (trimmed === \"\") return { kind: \"paragraph\", html: \"<p></p>\" };\n\n const heading = trimmed.match(SOLE_HEADING);\n if (heading) {\n const source = Number(heading[1]);\n const level = Math.min(source, 4) as HeadingLevel;\n return {\n kind: \"title\",\n level,\n inner: heading[2].trim(),\n ...(source > 4 ? { clampedFrom: source } : {}),\n };\n }\n\n return {\n kind: \"paragraph\",\n html: BLOCK_ROOT.test(trimmed) ? trimmed : `<p>${trimmed}</p>`,\n };\n}\n\nconst ENTITIES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n \""\": '\"',\n \"'\": \"'\",\n \"'\": \"'\",\n \" \": \" \",\n};\n\n/**\n * Remove every `<tag>` from `html`, keeping the text between them.\n *\n * A single-pass linear scan rather than `/<[^>]*>/g`: that regex is\n * polynomial over input with many `<` starts and no closing `>`, since\n * `[^>]*` backtracks at every one of them — `\"<\".repeat(n)` is quadratic in\n * `n`. `findOpenTagEnd` (from `@templatical/types`) resolves each `<` with a\n * bounded, quote-aware forward scan instead, so the walk stays O(n): every\n * character is visited at most once by `indexOf` and once by\n * `findOpenTagEnd`, because the cursor only moves forward and an\n * unterminated tag ends the walk rather than retrying from inside it. On\n * that unterminated case the remainder of the string — the dangling `<`\n * included — is emitted as-is, matching what `/<[^>]*>/g` does with\n * malformed markup: it can never complete a match without a `>`, so the\n * text is left untouched from that point on.\n */\nfunction removeTags(html: string): string {\n let out = \"\";\n let i = 0;\n while (i < html.length) {\n const open = html.indexOf(\"<\", i);\n if (open === -1) {\n out += html.substring(i);\n break;\n }\n out += html.substring(i, open);\n const tagEnd = findOpenTagEnd(html, open + 1);\n if (tagEnd === -1) {\n out += html.substring(open);\n break;\n }\n i = tagEnd + 1;\n }\n return out;\n}\n\n/**\n * The visible text of a markup fragment, for a button label — `ButtonBlock.text`\n * is a plain string, so the markup Topol stores in `content` cannot travel.\n */\nexport function stripTags(html: string | undefined): string {\n if (!html) return \"\";\n const text = removeTags(html);\n const decoded = text.replace(\n /&(?:amp|lt|gt|quot|#39|apos|nbsp);/g,\n (m) => ENTITIES[m] ?? m,\n );\n return decoded.replace(/\\s+/g, \" \").trim();\n}\n","import {\n canonicalFontStack,\n createButtonBlock,\n createDividerBlock,\n createHtmlBlock,\n createImageBlock,\n createParagraphBlock,\n createSpacerBlock,\n createTitleBlock,\n} from \"@templatical/types\";\nimport type { Block, BlockStyles } from \"@templatical/types\";\nimport {\n attr,\n numAttr,\n parseAlignment,\n parseBorderStyle,\n parseColor,\n parsePadding,\n parsePxValue,\n} from \"./attribute-parser\";\nimport { inferTextBlock, stripTags } from \"./content-inference\";\nimport { selectorDefault, tagDefault, type GlobalStyle } from \"./global-style\";\nimport type { ImportReportEntry, TopolNode } from \"./types\";\n\nexport interface MapContext {\n style: GlobalStyle;\n /** The rendered width of the column holding this node, for `width: \"full\"`. */\n columnWidth: number;\n}\n\nexport interface Converted {\n block: Block | null;\n entry: ImportReportEntry;\n}\n\n/**\n * Read a node's own attribute, falling back to the design's per-tag then\n * per-selector default. Returns undefined when nothing sets it, so the caller\n * can omit the field and let the block factory's default stand.\n */\nfunction resolved(\n node: TopolNode,\n key: string,\n ctx: MapContext,\n selector?: string,\n): string | undefined {\n return (\n attr(node, key) ??\n tagDefault(ctx.style, node.tagName, key) ??\n (selector === undefined\n ? undefined\n : selectorDefault(ctx.style, selector, key))\n );\n}\n\n/**\n * The `styles` every leaf block shares.\n *\n * The block background is the row behind the element, which MJML reads from\n * `container-background-color` on every leaf and which the renderer writes\n * `styles.backgroundColor` back out as. A leaf's `background-color` must not\n * feed it: on `mj-button` it is the button face (`backgroundColor`, read in\n * `convertButton`), on `mj-text` MJML never paints it, and on image, spacer,\n * divider and social it is invalid and dropped. Reading it here paints a\n * button's whole row in its fill colour. `buildSection` reads\n * `background-color` itself, since on `mj-section` it is the section's fill.\n */\nexport function baseStyles(node: TopolNode): { styles: BlockStyles } {\n const backgroundColor = parseColor(attr(node, \"container-background-color\"));\n return {\n styles: {\n padding: parsePadding(node),\n ...(backgroundColor ? { backgroundColor } : {}),\n },\n };\n}\n\nfunction convertText(node: TopolNode, ctx: MapContext): Converted {\n const inferred = inferTextBlock(node.content);\n\n if (inferred.kind === \"paragraph\") {\n return {\n block: createParagraphBlock({\n content: inferred.html,\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n }\n\n const selector = `h${inferred.level}`;\n const color = parseColor(resolved(node, \"color\", ctx, selector));\n const fontFamily = canonicalFontStack(\n resolved(node, \"font-family\", ctx, selector),\n );\n\n const block = createTitleBlock({\n content: inferred.inner,\n level: inferred.level,\n textAlign: parseAlignment(attr(node, \"align\"), \"left\"),\n ...(color ? { color } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseStyles(node),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"title\",\n status: inferred.clampedFrom ? \"approximated\" : \"converted\",\n ...(inferred.clampedFrom\n ? {\n note: `Heading level h${inferred.clampedFrom} clamped to 4 — Templatical titles support h1-h4.`,\n }\n : {}),\n },\n };\n}\n\nfunction convertButton(node: TopolNode, ctx: MapContext): Converted | null {\n const text = stripTags(node.content);\n if (!text) return null;\n\n const backgroundColor = parseColor(\n resolved(node, \"background-color\", ctx, \"button\"),\n );\n const textColor = parseColor(resolved(node, \"color\", ctx, \"button\"));\n const fontFamily = canonicalFontStack(\n resolved(node, \"font-family\", ctx, \"button\"),\n );\n const fontSizeRaw = resolved(node, \"font-size\", ctx);\n const radiusRaw = resolved(node, \"border-radius\", ctx);\n\n return {\n block: createButtonBlock({\n text,\n url: attr(node, \"href\") ?? \"\",\n ...(backgroundColor ? { backgroundColor } : {}),\n ...(textColor ? { textColor } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...(fontSizeRaw === undefined\n ? {}\n : { fontSize: parsePxValue(fontSizeRaw) }),\n ...(radiusRaw === undefined\n ? {}\n : { borderRadius: parsePxValue(radiusRaw) }),\n align: parseAlignment(attr(node, \"align\"), \"center\"),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: node.tagName,\n templaticalBlockType: \"button\",\n status: \"converted\",\n },\n };\n}\n\nfunction convertImage(\n node: TopolNode,\n ctx: MapContext,\n sourceTag: string,\n): Converted | null {\n const src = attr(node, \"src\");\n if (!src) return null;\n\n const widthPercent = numAttr(node, \"widthPercent\");\n const pxWidth = numAttr(node, \"width\");\n const href = attr(node, \"href\");\n const base = baseStyles(node);\n // MJML caps an image at its column less its own side padding, so a px width\n // that reaches that renders as \"full\" does.\n const room =\n ctx.columnWidth - base.styles.padding.left - base.styles.padding.right;\n\n const width =\n widthPercent === 100 || pxWidth === undefined || pxWidth >= room\n ? (\"full\" as const)\n : Math.round(pxWidth);\n\n const isGif = sourceTag === \"mj-gif\";\n\n return {\n block: createImageBlock({\n src,\n alt: attr(node, \"alt\") ?? \"\",\n width,\n align: parseAlignment(attr(node, \"align\"), \"center\"),\n ...(href ? { linkUrl: href } : {}),\n ...base,\n }),\n entry: {\n sourceTag,\n templaticalBlockType: \"image\",\n status: isGif ? \"approximated\" : \"converted\",\n ...(isGif\n ? {\n note: \"<mj-gif> imported as an image block; Templatical has no dedicated GIF block.\",\n }\n : {}),\n },\n };\n}\n\nfunction convertSpacer(node: TopolNode): Converted {\n const height = attr(node, \"height\");\n return {\n block: createSpacerBlock({\n ...(height === undefined ? {} : { height: parsePxValue(height) }),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-spacer\",\n templaticalBlockType: \"spacer\",\n status: \"converted\",\n },\n };\n}\n\nfunction convertDivider(node: TopolNode): Converted {\n const color = parseColor(attr(node, \"border-color\"));\n const thickness = attr(node, \"border-width\");\n return {\n block: createDividerBlock({\n lineStyle: parseBorderStyle(attr(node, \"border-style\")),\n ...(color ? { color } : {}),\n ...(thickness === undefined\n ? {}\n : { thickness: parsePxValue(thickness) }),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-divider\",\n templaticalBlockType: \"divider\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * Convert one leaf node to a block.\n *\n * Returns `null` for a node that produces nothing *and* warrants no report\n * entry — an image with no `src`, a button with no label. `mj-social` is not\n * handled here; the converter routes it to `convertSocial`.\n */\nexport function convertLeaf(\n node: TopolNode,\n ctx: MapContext,\n): Converted | null {\n switch (node.tagName) {\n case \"mj-text\":\n return convertText(node, ctx);\n case \"mj-button\":\n return convertButton(node, ctx);\n case \"mj-image\":\n return convertImage(node, ctx, \"mj-image\");\n case \"mj-gif\":\n return convertImage(node, ctx, \"mj-gif\");\n case \"mj-spacer\":\n return convertSpacer(node);\n case \"mj-divider\":\n return convertDivider(node);\n default:\n return {\n block: createHtmlBlock({\n content: JSON.stringify(node, null, 2),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: node.tagName,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `<${node.tagName}> has no Templatical block equivalent; the original node is preserved as JSON.`,\n },\n };\n }\n}\n","import { createSocialIconsBlock, generateId } from \"@templatical/types\";\nimport type {\n SocialIcon,\n SocialIconTone,\n SocialIconSize,\n SocialIconStyle,\n SocialPlatform,\n} from \"@templatical/types\";\nimport { attr, parseAlignment, parsePxValue } from \"./attribute-parser\";\nimport { baseStyles, type Converted, type MapContext } from \"./block-mapper\";\nimport type { TopolNode } from \"./types\";\n\n/**\n * Exhaustive over `SocialPlatform` on purpose: adding a member to that union\n * without adding it here is a compile error, so this cannot silently fall back\n * to \"website\" for a platform the block model gained.\n */\nconst KNOWN_PLATFORMS: Record<SocialPlatform, true> = {\n facebook: true,\n twitter: true,\n instagram: true,\n linkedin: true,\n youtube: true,\n tiktok: true,\n pinterest: true,\n email: true,\n whatsapp: true,\n telegram: true,\n discord: true,\n snapchat: true,\n reddit: true,\n github: true,\n dribbble: true,\n behance: true,\n website: true,\n};\n\nconst ALIASES: Record<string, SocialPlatform> = {\n x: \"twitter\",\n \"x-twitter\": \"twitter\",\n};\n\nconst ICON_SIZES: Array<[number, SocialIconSize]> = [\n [24, \"small\"],\n [32, \"medium\"],\n [48, \"large\"],\n];\n\nconst KNOWN_STYLES = new Set<string>([\n \"solid\",\n \"outlined\",\n \"rounded\",\n \"square\",\n \"circle\",\n]);\n\n// Topol ships icon sets whose folder name is not a Templatical style name.\n// `outlinedbw` is the outlined set in black and white, so it imports as\n// outlined in the dark tone; mapping it to the `solid` fallback would import\n// an outlined icon set as filled. Measured: both `/social-icos/outlined/` and\n// `/social-icos/outlinedbw/` appear across the reference exports.\nconst STYLE_ALIASES: Record<\n string,\n { style: SocialIconStyle; tone: SocialIconTone }\n> = {\n outlinedbw: { style: \"outlined\", tone: \"dark\" },\n};\n\nfunction normalizePlatform(raw: string): SocialPlatform | null {\n const cleaned = raw.trim().toLowerCase();\n if (!cleaned) return null;\n if (ALIASES[cleaned]) return ALIASES[cleaned];\n return cleaned in KNOWN_PLATFORMS ? (cleaned as SocialPlatform) : null;\n}\n\nfunction nearestSize(px: number): { size: SocialIconSize; exact: boolean } {\n let best = ICON_SIZES[1];\n let gap = Infinity;\n for (const candidate of ICON_SIZES) {\n const d = Math.abs(candidate[0] - px);\n if (d < gap) {\n gap = d;\n best = candidate;\n }\n }\n return { size: best[1], exact: gap === 0 };\n}\n\n/** The style (and tone) the segment of `https://…/social-icos/<style>/` names. */\nfunction styleFromBaseUrl(\n baseUrl: string | undefined,\n): { style: SocialIconStyle; tone?: SocialIconTone } | undefined {\n if (!baseUrl) return undefined;\n const segment = baseUrl.split(\"?\")[0].split(\"/\").filter(Boolean).at(-1) ?? \"\";\n if (KNOWN_STYLES.has(segment)) return { style: segment as SocialIconStyle };\n return STYLE_ALIASES[segment];\n}\n\n/**\n * Convert Topol's MJML v3 social block.\n *\n * `display` decides which icons render and in what order; a platform with\n * `*-href` set but absent from `display` was not shown in the email and is\n * not imported.\n */\nexport function convertSocial(\n node: TopolNode,\n _ctx: MapContext,\n): Converted | null {\n const display = attr(node, \"display\");\n if (!display) return null;\n\n const notes: string[] = [];\n const icons: SocialIcon[] = [];\n\n for (const token of display.trim().split(/\\s+/)) {\n const rawName = token.split(\":\")[0];\n if (!rawName) continue;\n\n const url = attr(node, `${rawName}-href`);\n if (!url) continue;\n\n const platform = normalizePlatform(rawName);\n if (!platform) {\n notes.push(\n `Unrecognised social platform \"${rawName}\" mapped to \"website\".`,\n );\n }\n\n icons.push({ id: generateId(), platform: platform ?? \"website\", url });\n }\n\n if (icons.length === 0) return null;\n\n const fromBaseUrl = styleFromBaseUrl(attr(node, \"base-url\"));\n\n const declaredSize = attr(node, \"icon-size\");\n let iconSize: SocialIconSize | undefined;\n if (declaredSize !== undefined) {\n const px = parsePxValue(declaredSize);\n const resolved = nearestSize(px);\n iconSize = resolved.size;\n if (!resolved.exact) {\n notes.push(\n `Icon size ${declaredSize} is not one of 24/32/48px; resolved to \"${resolved.size}\".`,\n );\n }\n }\n\n return {\n block: createSocialIconsBlock({\n icons,\n align: parseAlignment(attr(node, \"align\"), \"center\"),\n ...(iconSize ? { iconSize } : {}),\n ...(fromBaseUrl ? { iconStyle: fromBaseUrl.style } : {}),\n ...(fromBaseUrl?.tone ? { iconTone: fromBaseUrl.tone } : {}),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-social\",\n templaticalBlockType: \"social\",\n status: notes.length > 0 ? \"approximated\" : \"converted\",\n ...(notes.length > 0 ? { note: notes.join(\" \") } : {}),\n },\n };\n}\n","import { createSectionBlock } from \"@templatical/types\";\nimport type { Block, ColumnLayout } from \"@templatical/types\";\nimport {\n attr,\n parseColor,\n parsePadding,\n parsePercent,\n readMjmlBackgroundImage,\n} from \"./attribute-parser\";\nimport { convertLeaf, type MapContext } from \"./block-mapper\";\nimport { convertSocial } from \"./social-mapper\";\nimport type { ImportReportEntry, TopolNode } from \"./types\";\n\nconst LAYOUT_SHAPES: Array<{ layout: ColumnLayout; percents: number[] }> = [\n { layout: \"1\", percents: [100] },\n { layout: \"2\", percents: [50, 50] },\n { layout: \"1-2\", percents: [33.33, 66.67] },\n { layout: \"2-1\", percents: [66.67, 33.33] },\n { layout: \"3\", percents: [33.33, 33.33, 33.34] },\n];\n\nconst WIDTH_TOLERANCE = 2;\n\nconst COLUMN_COUNT: Record<ColumnLayout, number> = {\n \"1\": 1,\n \"2\": 2,\n \"3\": 3,\n \"2-1\": 2,\n \"1-2\": 2,\n};\n\nfunction columnPixels(layout: ColumnLayout, container: number): number[] {\n switch (layout) {\n case \"2\":\n return [container / 2, container / 2];\n case \"3\":\n return [container / 3, container / 3, container / 3];\n case \"1-2\":\n return [container / 3, (container * 2) / 3];\n case \"2-1\":\n return [(container * 2) / 3, container / 3];\n default:\n return [container];\n }\n}\n\n/**\n * Resolve column widths to one of the five layouts `ColumnLayout` allows.\n *\n * `exact: false` means the caller reports `approximated` — Topol permits any\n * column count at any width and this union permits five shapes, so this is the\n * importer's main irreducible loss.\n */\nexport function matchColumnLayout(percents: Array<number | null>): {\n layout: ColumnLayout;\n exact: boolean;\n} {\n const count = percents.length;\n if (count === 0) return { layout: \"1\", exact: true };\n\n // All-unset is \"distribute equally\", which is exactly what the n-column\n // layout means — not a missing value to approximate around.\n if (percents.every((p) => p === null)) {\n if (count === 1) return { layout: \"1\", exact: true };\n if (count === 2) return { layout: \"2\", exact: true };\n if (count === 3) return { layout: \"3\", exact: true };\n return { layout: \"3\", exact: false };\n }\n\n const known = percents.filter((p): p is number => p !== null);\n const unsetCount = count - known.length;\n const remainder =\n unsetCount === 0\n ? 0\n : Math.max(0, 100 - known.reduce((a, b) => a + b, 0)) / unsetCount;\n const resolved = percents.map((p) => p ?? remainder);\n\n const sameCount = LAYOUT_SHAPES.filter((s) => s.percents.length === count);\n\n for (const shape of sameCount) {\n if (\n shape.percents.every(\n (want, i) => Math.abs(want - resolved[i]) <= WIDTH_TOLERANCE,\n )\n ) {\n return { layout: shape.layout, exact: true };\n }\n }\n\n const candidates =\n sameCount.length > 0\n ? sameCount\n : LAYOUT_SHAPES.filter((s) => s.layout === \"3\");\n\n let best = candidates[0];\n let bestError = Infinity;\n for (const shape of candidates) {\n const error = shape.percents.reduce(\n (sum, want, i) => sum + Math.abs(want - (resolved[i] ?? 0)),\n 0,\n );\n if (error < bestError) {\n bestError = error;\n best = shape;\n }\n }\n\n return { layout: best.layout, exact: false };\n}\n\nfunction convertColumnChildren(\n column: TopolNode,\n ctx: MapContext,\n entries: ImportReportEntry[],\n): Block[] {\n const blocks: Block[] = [];\n for (const child of column.children ?? []) {\n const converted =\n child.tagName === \"mj-social\"\n ? convertSocial(child, ctx)\n : convertLeaf(child, ctx);\n if (!converted) continue;\n entries.push(converted.entry);\n if (converted.block) blocks.push(converted.block);\n }\n return blocks;\n}\n\n/**\n * Build a `SectionBlock` from an `mj-section`. Returns an array so the caller\n * can treat an empty section (no columns) as producing nothing.\n *\n * Reads `attributes` only. The node's top-level camelCase keys\n * (`backgroundColor`, `paddingTop`, …) are stale editor state that disagrees\n * with `attributes` in every measured case.\n */\nexport function buildSection(\n node: TopolNode,\n ctx: MapContext,\n entries: ImportReportEntry[],\n): Block[] {\n const columns = (node.children ?? []).filter(\n (c) => c.tagName === \"mj-column\",\n );\n if (columns.length === 0) return [];\n\n const rawWidths = columns.map((c) => attr(c, \"width\"));\n const percents = rawWidths.map((w) => parsePercent(w));\n const { layout, exact } = matchColumnLayout(percents);\n\n const slots = COLUMN_COUNT[layout];\n const pixels = columnPixels(layout, ctx.style.settings.width);\n const children: Block[][] = Array.from({ length: slots }, () => []);\n\n const background = readMjmlBackgroundImage(\n (name) => attr(node, name),\n \"Section\",\n ctx.style.settings.width,\n );\n const notes = [\n ...(exact\n ? []\n : [\n `Column widths ${rawWidths.map((w) => w ?? \"auto\").join(\", \")} have no exact Templatical layout; resolved to \"${layout}\".`,\n ]),\n ...(background?.notes ?? []),\n ];\n entries.push({\n sourceTag: \"mj-section\",\n templaticalBlockType: \"section\",\n status: notes.length > 0 ? \"approximated\" : \"converted\",\n ...(notes.length > 0 ? { note: notes.join(\" \") } : {}),\n });\n\n columns.forEach((column, index) => {\n const slot = Math.min(index, slots - 1);\n const columnCtx: MapContext = {\n ...ctx,\n columnWidth: Math.round(pixels[slot] ?? ctx.style.settings.width),\n };\n children[slot].push(...convertColumnChildren(column, columnCtx, entries));\n });\n\n const backgroundColor = parseColor(attr(node, \"background-color\"));\n\n return [\n createSectionBlock({\n columns: layout,\n children,\n styles: {\n padding: parsePadding(node),\n ...(backgroundColor ? { backgroundColor } : {}),\n },\n ...(background ? { backgroundImage: background.image } : {}),\n }),\n ];\n}\n","import { createDefaultTemplateContent } from \"@templatical/types\";\nimport type { Block, TemplateContent } from \"@templatical/types\";\nimport { readGlobalStyle } from \"./global-style\";\nimport { buildSection } from \"./section-builder\";\nimport type { MapContext } from \"./block-mapper\";\nimport type {\n ImportReport,\n ImportReportEntry,\n ImportResult,\n TopolDesign,\n} from \"./types\";\n\nconst ROOT_TAG = \"mj-global-style\";\n\nconst INVALID_DESIGN_MESSAGE =\n 'Invalid Topol template: expected the design JSON object — Topol hands you this directly from its editor\\'s onSave callback, or under \"definition\" (template API) or \"json\" (predefined-templates API) in its API response.';\n\nconst EMPTY_DESIGN_WARNING =\n \"No convertible content was found in the Topol design. Check that the mj-container holds at least one mj-section.\";\n\n/**\n * Convert a Topol.io design into a Templatical template.\n *\n * The input is the design object itself — the node whose `tagName` is\n * `mj-global-style`, not a whole API response. Topol's editor hands it to\n * you directly as the first argument to `onSave`; its REST APIs wrap it\n * instead, under `definition` on the template-retrieval endpoint or `json`\n * (alongside `html`) on the predefined-templates endpoint. That is\n * documented rather than sniffed: guessing which field holds the design\n * risks importing an envelope's `html`, which belongs to a different\n * package entirely.\n *\n * @example\n * ```ts\n * const { content, report } = convertTopolTemplate(design);\n * const editor = init({ container: '#editor', content });\n * console.log(report.summary);\n * ```\n */\nexport function convertTopolTemplate(\n design: TopolDesign | string,\n): ImportResult {\n const root: unknown = typeof design === \"string\" ? safeParse(design) : design;\n\n if (typeof root !== \"object\" || root === null || Array.isArray(root)) {\n throw new Error(INVALID_DESIGN_MESSAGE);\n }\n\n const topolDesign = root as TopolDesign;\n\n if (topolDesign.tagName !== ROOT_TAG) {\n // Topol's REST APIs wrap the design rather than returning it bare: the\n // template-retrieval endpoint uses \"definition\", the predefined/premade\n // templates endpoint uses \"json\" (alongside \"html\"). A response object\n // passed here by mistake still carries one of those keys, so that\n // mistake is named directly instead of falling through to the generic\n // wrong-tagName message below.\n if (\"json\" in topolDesign || \"definition\" in topolDesign) {\n throw new Error(INVALID_DESIGN_MESSAGE);\n }\n throw new Error(\n `Invalid Topol template: expected a root node with tagName \"${ROOT_TAG}\".`,\n );\n }\n\n const container = (topolDesign.children ?? []).find(\n (child) => child.tagName === \"mj-container\",\n );\n\n const entries: ImportReportEntry[] = [];\n const warnings: string[] = [];\n const blocks: Block[] = [];\n\n const style = readGlobalStyle(topolDesign, container, warnings);\n\n const ctx: MapContext = {\n style,\n columnWidth: style.settings.width,\n };\n\n for (const child of container?.children ?? []) {\n if (child.tagName !== \"mj-section\") continue;\n blocks.push(...buildSection(child, ctx, entries));\n }\n\n if (blocks.length === 0) {\n warnings.push(EMPTY_DESIGN_WARNING);\n }\n\n const content: TemplateContent = {\n ...createDefaultTemplateContent(),\n blocks,\n settings: style.settings,\n };\n\n const summary = {\n total: entries.length,\n converted: entries.filter((e) => e.status === \"converted\").length,\n approximated: entries.filter((e) => e.status === \"approximated\").length,\n htmlFallback: entries.filter((e) => e.status === \"html-fallback\").length,\n skipped: entries.filter((e) => e.status === \"skipped\").length,\n };\n\n const report: ImportReport = { entries, warnings, summary };\n\n return { content, report };\n}\n\nfunction safeParse(source: string): unknown {\n try {\n return JSON.parse(source);\n } catch {\n throw new Error(INVALID_DESIGN_MESSAGE);\n }\n}\n"],"mappings":";;;;;;;;;;AAiBA,SAAgB,KAAK,MAAiB,KAAiC;CACrE,MAAM,MAAM,KAAK,aAAa;CAC9B,IAAI,QAAQ,QAAQ,QAAQ,KAAA,GAAW,OAAO,KAAA;CAC9C,IAAI,OAAO,QAAQ,UAAU,OAAO,KAAA;CACpC,MAAM,QAAQ,OAAO,GAAG;CACxB,OAAO,UAAU,KAAK,KAAA,IAAY;AACpC;;AAGA,SAAgB,QAAQ,MAAiB,KAAiC;CACxE,MAAM,QAAQ,KAAK,MAAM,GAAG;CAC5B,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;CAChC,MAAM,SAAS,OAAO,KAAK;CAC3B,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS,KAAA;AAC5C;;;;;;;;;;;;;;AAeA,SAAgB,aAAa,OAA4C;CACvE,IAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,IAAI,OAAO;CAClE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAK,MAAM,KAAK;CACtD,MAAM,UAAU,MAAM,KAAK;CAC3B,MAAM,QAAQ,6BAA6B,KAAK,OAAO;CACvD,OAAO,QAAQ,KAAK,MAAM,WAAW,MAAM,EAAE,CAAC,IAAI;AACpD;AAEA,MAAM,eAAuC;CAC3C,OAAO;CACP,OAAO;CACP,KAAK;CACL,OAAO;CACP,MAAM;CACN,QAAQ;CACR,MAAM;CACN,SAAS;CACT,MAAM;CACN,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,OAAO;CACP,MAAM;CACN,MAAM;CACN,MAAM;CACN,MAAM;CACN,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,MAAM;AACR;AAEA,SAAS,SAAS,GAAW,GAAW,GAAmB;CACzD,MAAM,SAAS,MAAc,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC;CACrE,MAAM,OAAO,MAAc,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;CAChE,OAAO,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC;AACpC;;;;;;;;AASA,SAAgB,WAAW,OAAmC;CAC5D,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,UAAU,MAAM,KAAK,CAAC,CAAC,YAAY;CACzC,IAAI,YAAY,iBAAiB,YAAY,aAAa,YAAY,QACpE,OAAO;CACT,IAAI,iBAAiB,KAAK,OAAO,GAAG,OAAO;CAC3C,IAAI,iBAAiB,KAAK,OAAO,GAAG;EAClC,MAAM,GAAG,GAAG,GAAG,KAAK;EACpB,OAAO,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;CACjC;CACA,MAAM,MAAM,QAAQ,MAClB,kEACF;CACA,IAAI,KAAK,OAAO,SAAS,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE;CAClD,OAAO,aAAa,YAAY;AAClC;AAEA,SAAS,gBAAgB,OAAyC;CAChE,IAAI,CAAC,OAAO,OAAO;EAAE,KAAK;EAAG,OAAO;EAAG,QAAQ;EAAG,MAAM;CAAE;CAC1D,MAAM,IAAI,MACP,KAAK,CAAC,CACN,MAAM,KAAK,CAAC,CACZ,KAAK,MAAM,aAAa,CAAC,CAAC;CAC7B,QAAQ,EAAE,QAAV;EACE,KAAK,GACH,OAAO;GAAE,KAAK,EAAE;GAAI,OAAO,EAAE;GAAI,QAAQ,EAAE;GAAI,MAAM,EAAE;EAAG;EAC5D,KAAK,GACH,OAAO;GAAE,KAAK,EAAE;GAAI,OAAO,EAAE;GAAI,QAAQ,EAAE;GAAI,MAAM,EAAE;EAAG;EAC5D,KAAK,GACH,OAAO;GAAE,KAAK,EAAE;GAAI,OAAO,EAAE;GAAI,QAAQ,EAAE;GAAI,MAAM,EAAE;EAAG;EAC5D,SACE,OAAO;GAAE,KAAK,EAAE;GAAI,OAAO,EAAE;GAAI,QAAQ,EAAE;GAAI,MAAM,EAAE;EAAG;CAC9D;AACF;;;;;;;AAQA,SAAgB,aAAa,MAA+B;CAC1D,MAAM,OAAO,gBAAgB,KAAK,MAAM,SAAS,CAAC;CAClD,MAAM,QAAQ,KAAa,aAAqB;EAC9C,MAAM,QAAQ,KAAK,MAAM,GAAG;EAC5B,OAAO,UAAU,KAAA,IAAY,WAAW,aAAa,KAAK;CAC5D;CACA,OAAO;EACL,KAAK,KAAK,eAAe,KAAK,GAAG;EACjC,OAAO,KAAK,iBAAiB,KAAK,KAAK;EACvC,QAAQ,KAAK,kBAAkB,KAAK,MAAM;EAC1C,MAAM,KAAK,gBAAgB,KAAK,IAAI;CACtC;AACF;;;;;;AAOA,SAAgB,aACd,OACe;CACf,IAAI,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,IAAI,OAAO;CAClE,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;CACvE,MAAM,QAAQ,MAAM,KAAK,CAAC,CAAC,MAAM,uBAAuB;CACxD,OAAO,QAAQ,WAAW,MAAM,EAAE,IAAI;AACxC;;AAGA,SAAgB,eACd,OACA,WAAwC,QACX;CAC7B,MAAM,KAAK,SAAS,GAAA,CAAI,KAAK,CAAC,CAAC,YAAY;CAC3C,OAAO,MAAM,UAAU,MAAM,YAAY,MAAM,UAAU,IAAI;AAC/D;;AAGA,SAAgB,iBACd,OAC+B;CAC/B,MAAM,KAAK,SAAS,GAAA,CAAI,KAAK,CAAC,CAAC,YAAY;CAC3C,OAAO,MAAM,YAAY,MAAM,WAAW,IAAI;AAChD;AAQA,MAAM,SAAS;CAAC;CAAQ;CAAU;AAAO;AACzC,MAAM,SAAS;CAAC;CAAO;CAAU;AAAQ;AACzC,MAAM,UAAU;AAChB,MAAM,SAAS;;AAGf,MAAM,2BAA2B;CAC/B,MAAM;CACN,GAAG;CACH,GAAG;CACH,QAAQ;AACV;;;;;;AAOA,SAAS,sBACP,KACA,UAC0C;CAC1C,MAAM,QAAQ,IAAI,YAAY,CAAC,CAAC,MAAM,KAAK;CAC3C,MAAM,CAAC,SAAS;CAChB,IAAI,MAAM,WAAW,MAAM,UAAU,WAAW,UAAU,YACxD,OAAO;EAAE,MAAM;EAAO,OAAO;CAAK;CAEpC,IAAI,MAAM,OAAO,SAAS,SAAS,MAAM,GACvC,OAAO;EAAE,MAAM;EAAQ,OAAO;CAAK;CAErC,MAAM,KAAK,4BAA4B,KAAK,KAAK;CAIjD,OAAO;EAAE,MAFP,MAAM,SAAS,MAAM,KACpB,OAAO,QAAQ,aAAa,KAAA,KAAa,OAAO,GAAG,EAAE,KAAK,WACtC,UAAU;EAAQ,OAAO;CAAM;AACxD;;;;;;;;AASA,SAAS,wBAAwB,KAI/B;CACA,MAAM,QAAQ,IAAI,YAAY,CAAC,CAAC,MAAM,KAAK;CAC3C,IAAI,MAAM,WAAW,GAAG;EACtB,MAAM,CAAC,QAAQ;EACf,OAAO,SAAS,SAAS,SAAS,WAC9B;GAAE,GAAG;GAAU,GAAG;GAAM,OAAO;EAAK,IACpC;GAAE,GAAG;GAAM,GAAG;GAAU,OAAO;EAAK;CAC1C;CACA,IAAI,MAAM,WAAW,GAAG;EACtB,MAAM,CAAC,GAAG,KAAK;EAKf,OAHE,MAAM,SACN,MAAM,YACL,MAAM,aAAa,MAAM,UAAU,MAAM,WAC5B;GAAE,GAAG;GAAG,GAAG;GAAG,OAAO;EAAK,IAAI;GAAE,GAAG;GAAG,GAAG;GAAG,OAAO;EAAK;CAC1E;CACA,OAAO;EACL,GAAG,MAAM,MAAM,SAAS,SAAS,UAAU,SAAS,OAAO,KAAK;EAChE,GAAG,MAAM,MAAM,SAAS,SAAS,SAAS,SAAS,QAAQ,KAAK;EAChE,OAAO;CACT;AACF;;;;;;AAOA,SAAS,oBACP,OACA,MACqC;CACrC,MAAM,UAAU,KAAK,MAAM,UAAU,UAAU,KAAK;CACpD,IAAI,SAAS,OAAO;EAAE,OAAO;EAAS,OAAO;CAAK;CAClD,MAAM,UAAU,QAAQ,KAAK,KAAK;CAClC,IAAI,SAAS;EACX,MAAM,IAAI,OAAO,QAAQ,EAAE;EAC3B,OAAO;GACL,OAAO,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,KAAK;GAClD,OAAO,MAAM,KAAK,MAAM,MAAM,MAAM;EACtC;CACF;CACA,IAAI,OAAO,KAAK,KAAK,GACnB,OAAO;EAAE,OAAO,KAAK;EAAI,OAAO,WAAW,KAAK,MAAM;CAAE;CAE1D,OAAO;AACT;;;;;AAMA,SAAS,qBAAqB,KAG5B;CACA,MAAM,QAAQ,IAAI,YAAY,CAAC,CAAC,MAAM,KAAK;CAC3C,IAAI,MAAM,OAAO,SAAS,SAAS,WAAW,GAC5C,OAAO;EAAE,QAAQ;EAAO,OAAO;CAAK;CAEtC,OAAO;EAAE,QAAQ;EAAM,OAAO,MAAM,OAAO,SAAS,SAAS,QAAQ;CAAE;AACzE;;;;;;;;;AAUA,SAAgB,wBACd,MACA,OACA,UAC4B;CAC5B,MAAM,OAAO,KAAK,gBAAgB,KAAK,GAAA,CAAI,KAAK;CAChD,IAAI,CAAC,KAAK,OAAO;CAEjB,MAAM,QAAkB,CAAC;CACzB,MAAM,SAAS,MAAc,KAAa,UAAwB;EAChE,MAAM,KACJ,GAAG,MAAM,GAAG,KAAK,IAAI,IAAI,sDAAsD,MAAM,GACvF;CACF;CAEA,IAAI,OAAuB,yBAAyB;CACpD,MAAM,UAAU,KAAK,iBAAiB,CAAC,EAAE,KAAK;CAC9C,IAAI,SAAS;EACX,MAAM,SAAS,sBAAsB,SAAS,QAAQ;EACtD,OAAO,OAAO;EACd,IAAI,CAAC,OAAO,OAAO,MAAM,mBAAmB,SAAS,IAAI;CAC3D;CAEA,IAAI,IAAyB,yBAAyB;CACtD,IAAI,IAAyB,yBAAyB;CACtD,IAAI,SAAS;CACb,IAAI,SAAS;CACb,MAAM,cAAc,KAAK,qBAAqB,CAAC,EAAE,KAAK;CACtD,IAAI,aAAa;EACf,MAAM,QAAQ,wBAAwB,WAAW;EACjD,MAAM,QAAQ,oBAAoB,MAAM,GAAG,MAAM;EACjD,MAAM,QAAQ,oBAAoB,MAAM,GAAG,MAAM;EACjD,IAAI,OAAO,SAAS;EACpB,IAAI,OAAO,SAAS;EACpB,SAAS,CAAC,MAAM,SAAS,CAAC,OAAO;EACjC,SAAS,CAAC,MAAM,SAAS,CAAC,OAAO;CACnC;CAGA,MAAM,OAAO,KAAK,uBAAuB,CAAC,EAAE,KAAK;CACjD,MAAM,YAAY,OACd,oBAAoB,KAAK,YAAY,GAAG,MAAM,IAC9C;CACJ,IAAI,WAAW;EACb,IAAI,UAAU;EACd,SAAS;CACX;CACA,MAAM,OAAO,KAAK,uBAAuB,CAAC,EAAE,KAAK;CACjD,MAAM,YAAY,OACd,oBAAoB,KAAK,YAAY,GAAG,MAAM,IAC9C;CACJ,IAAI,WAAW;EACb,IAAI,UAAU;EACd,SAAS;CACX;CACA,IAAI,gBAAgB,UAAU,SAC5B,MAAM,uBAAuB,aAAa,GAAG,EAAE,GAAG,GAAG;CAEvD,IAAI,QAAQ,CAAC,WAAW,OAAO,MAAM,yBAAyB,MAAM,CAAC;CACrE,IAAI,QAAQ,CAAC,WAAW,OAAO,MAAM,yBAAyB,MAAM,CAAC;CAErE,IAAI,SAAkB,yBAAyB;CAC/C,MAAM,YAAY,KAAK,mBAAmB,CAAC,EAAE,KAAK;CAClD,IAAI,WAAW;EACb,MAAM,SAAS,qBAAqB,SAAS;EAC7C,SAAS,OAAO;EAChB,IAAI,CAAC,OAAO,OACV,MAAM,qBAAqB,WAAW,SAAS,WAAW,WAAW;CAEzE;CAEA,OAAO;EAAE,OAAO;GAAE;GAAK;GAAM,UAAU;IAAE;IAAG;GAAE;GAAG;EAAO;EAAG;CAAM;AACnE;;;;;;;;;;;ACtWA,MAAM,oBAAoB;;AAqB1B,SAAgB,WACd,OACA,KACA,KACoB;CACpB,OAAO,MAAM,YAAY,IAAI,GAAG;AAClC;;AAGA,SAAgB,gBACd,OACA,UACA,KACoB;CACpB,OAAO,MAAM,iBAAiB,SAAS,GAAG;AAC5C;;;;;;;;;AAUA,SAAgB,gBACd,MACA,WACA,UACa;CACb,MAAM,cAAsD,OAAO,OACjE,IACF;CACA,MAAM,mBAGF,OAAO,OAAO,IAAI;CAEtB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,cAAc,CAAC,CAAC,GAAG;EAChE,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;GAC/C,MAAM,SAAU,YAAY,SAAS,OAAO,OAAO,IAAI;GACvD,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,KAAK,GACvC,IAAI,MAAM,QAAQ,MAAM,KAAA,GAAW,OAAO,KAAK,OAAO,CAAC;GAEzD;EACF;EAEA,MAAM,QAAQ,IAAI,QAAQ,GAAG;EAI7B,IAAI,UAAU,IAAI;EAClB,MAAM,WAAW,IAAI,MAAM,GAAG,KAAK;EACnC,MAAM,WAAW,IAAI,MAAM,QAAQ,CAAC;EACpC,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW;EAC3C,MAAM,SAAU,iBAAiB,cAAc,OAAO,OAAO,IAAI;EACjE,OAAO,YAAY,OAAO,KAAK;CACjC;CAKA,MAAM,QACJ,aAAa,KAAK,MAAM,gBAAgB,CAAC,KAAK,kBAAkB;CAElE,MAAM,mBACH,YAAY,WAAW,KAAK,WAAW,kBAAkB,CAAC,IAAI,OAC/D,kBAAkB;CAEpB,MAAM,YACJ,WAAW,iBAAiB,GAAG,EAAE,KAAK,KAAK,kBAAkB;CAE/D,MAAM,aACJ,mBAAmB,iBAAiB,GAAG,GAAG,cAAc,KACxD,mBAAmB,KAAK,MAAM,OAAO,CAAC,KACtC,kBAAkB;CAEpB,MAAM,YAAY,WAAW,iBAAiB,IAAI,EAAE,KAAK;CAEzD,MAAM,aAAa,iBAAiB,GAAG,GAAG;CAC1C,IAAI,eAAe,KAAA,GACjB,SAAS,KACP,qCAAqC,WAAW,2DAClD;CAaF,OAAO;EAAE,UAAA;GATP;GACA;GACA;GACA,eAAe,kBAAkB;GACjC;GACA,QAAQ,kBAAkB;GAC1B,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;EAGnB;EAAG;EAAa;CAAiB;AACnD;;;;;;;;AC1HA,MAAM,eAAe;AAErB,MAAM,aAAa;;;;;;;;;AAUnB,SAAgB,eAAe,SAA2C;CACxE,MAAM,WAAW,WAAW,GAAA,CAAI,KAAK;CACrC,IAAI,YAAY,IAAI,OAAO;EAAE,MAAM;EAAa,MAAM;CAAU;CAEhE,MAAM,UAAU,QAAQ,MAAM,YAAY;CAC1C,IAAI,SAAS;EACX,MAAM,SAAS,OAAO,QAAQ,EAAE;EAEhC,OAAO;GACL,MAAM;GACN,OAHY,KAAK,IAAI,QAAQ,CAGzB;GACJ,OAAO,QAAQ,EAAE,CAAC,KAAK;GACvB,GAAI,SAAS,IAAI,EAAE,aAAa,OAAO,IAAI,CAAC;EAC9C;CACF;CAEA,OAAO;EACL,MAAM;EACN,MAAM,WAAW,KAAK,OAAO,IAAI,UAAU,MAAM,QAAQ;CAC3D;AACF;AAEA,MAAM,WAAmC;CACvC,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,SAAS;CACT,UAAU;CACV,UAAU;AACZ;;;;;;;;;;;;;;;;;AAkBA,SAAS,WAAW,MAAsB;CACxC,IAAI,MAAM;CACV,IAAI,IAAI;CACR,OAAO,IAAI,KAAK,QAAQ;EACtB,MAAM,OAAO,KAAK,QAAQ,KAAK,CAAC;EAChC,IAAI,SAAS,IAAI;GACf,OAAO,KAAK,UAAU,CAAC;GACvB;EACF;EACA,OAAO,KAAK,UAAU,GAAG,IAAI;EAC7B,MAAM,SAAS,eAAe,MAAM,OAAO,CAAC;EAC5C,IAAI,WAAW,IAAI;GACjB,OAAO,KAAK,UAAU,IAAI;GAC1B;EACF;EACA,IAAI,SAAS;CACf;CACA,OAAO;AACT;;;;;AAMA,SAAgB,UAAU,MAAkC;CAC1D,IAAI,CAAC,MAAM,OAAO;CAMlB,OALa,WAAW,IACL,CAAC,CAAC,QACnB,wCACC,MAAM,SAAS,MAAM,CAEX,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;AAC3C;;;;;;;;AChEA,SAAS,SACP,MACA,KACA,KACA,UACoB;CACpB,OACE,KAAK,MAAM,GAAG,KACd,WAAW,IAAI,OAAO,KAAK,SAAS,GAAG,MACtC,aAAa,KAAA,IACV,KAAA,IACA,gBAAgB,IAAI,OAAO,UAAU,GAAG;AAEhD;;;;;;;;;;;;;AAcA,SAAgB,WAAW,MAA0C;CACnE,MAAM,kBAAkB,WAAW,KAAK,MAAM,4BAA4B,CAAC;CAC3E,OAAO,EACL,QAAQ;EACN,SAAS,aAAa,IAAI;EAC1B,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;CAC/C,EACF;AACF;AAEA,SAAS,YAAY,MAAiB,KAA4B;CAChE,MAAM,WAAW,eAAe,KAAK,OAAO;CAE5C,IAAI,SAAS,SAAS,aACpB,OAAO;EACL,OAAO,qBAAqB;GAC1B,SAAS,SAAS;GAClB,GAAG,WAAW,IAAI;EACpB,CAAC;EACD,OAAO;GACL,WAAW;GACX,sBAAsB;GACtB,QAAQ;EACV;CACF;CAGF,MAAM,WAAW,IAAI,SAAS;CAC9B,MAAM,QAAQ,WAAW,SAAS,MAAM,SAAS,KAAK,QAAQ,CAAC;CAC/D,MAAM,aAAa,mBACjB,SAAS,MAAM,eAAe,KAAK,QAAQ,CAC7C;CAWA,OAAO;EACL,OAVY,iBAAiB;GAC7B,SAAS,SAAS;GAClB,OAAO,SAAS;GAChB,WAAW,eAAe,KAAK,MAAM,OAAO,GAAG,MAAM;GACrD,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GACzB,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;GACnC,GAAG,WAAW,IAAI;EACpB,CAGM;EACJ,OAAO;GACL,WAAW;GACX,sBAAsB;GACtB,QAAQ,SAAS,cAAc,iBAAiB;GAChD,GAAI,SAAS,cACT,EACE,MAAM,kBAAkB,SAAS,YAAY,mDAC/C,IACA,CAAC;EACP;CACF;AACF;AAEA,SAAS,cAAc,MAAiB,KAAmC;CACzE,MAAM,OAAO,UAAU,KAAK,OAAO;CACnC,IAAI,CAAC,MAAM,OAAO;CAElB,MAAM,kBAAkB,WACtB,SAAS,MAAM,oBAAoB,KAAK,QAAQ,CAClD;CACA,MAAM,YAAY,WAAW,SAAS,MAAM,SAAS,KAAK,QAAQ,CAAC;CACnE,MAAM,aAAa,mBACjB,SAAS,MAAM,eAAe,KAAK,QAAQ,CAC7C;CACA,MAAM,cAAc,SAAS,MAAM,aAAa,GAAG;CACnD,MAAM,YAAY,SAAS,MAAM,iBAAiB,GAAG;CAErD,OAAO;EACL,OAAO,kBAAkB;GACvB;GACA,KAAK,KAAK,MAAM,MAAM,KAAK;GAC3B,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;GAC7C,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;GACjC,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;GACnC,GAAI,gBAAgB,KAAA,IAChB,CAAC,IACD,EAAE,UAAU,aAAa,WAAW,EAAE;GAC1C,GAAI,cAAc,KAAA,IACd,CAAC,IACD,EAAE,cAAc,aAAa,SAAS,EAAE;GAC5C,OAAO,eAAe,KAAK,MAAM,OAAO,GAAG,QAAQ;GACnD,GAAG,WAAW,IAAI;EACpB,CAAC;EACD,OAAO;GACL,WAAW,KAAK;GAChB,sBAAsB;GACtB,QAAQ;EACV;CACF;AACF;AAEA,SAAS,aACP,MACA,KACA,WACkB;CAClB,MAAM,MAAM,KAAK,MAAM,KAAK;CAC5B,IAAI,CAAC,KAAK,OAAO;CAEjB,MAAM,eAAe,QAAQ,MAAM,cAAc;CACjD,MAAM,UAAU,QAAQ,MAAM,OAAO;CACrC,MAAM,OAAO,KAAK,MAAM,MAAM;CAC9B,MAAM,OAAO,WAAW,IAAI;CAG5B,MAAM,OACJ,IAAI,cAAc,KAAK,OAAO,QAAQ,OAAO,KAAK,OAAO,QAAQ;CAEnE,MAAM,QACJ,iBAAiB,OAAO,YAAY,KAAA,KAAa,WAAW,OACvD,SACD,KAAK,MAAM,OAAO;CAExB,MAAM,QAAQ,cAAc;CAE5B,OAAO;EACL,OAAO,iBAAiB;GACtB;GACA,KAAK,KAAK,MAAM,KAAK,KAAK;GAC1B;GACA,OAAO,eAAe,KAAK,MAAM,OAAO,GAAG,QAAQ;GACnD,GAAI,OAAO,EAAE,SAAS,KAAK,IAAI,CAAC;GAChC,GAAG;EACL,CAAC;EACD,OAAO;GACL;GACA,sBAAsB;GACtB,QAAQ,QAAQ,iBAAiB;GACjC,GAAI,QACA,EACE,MAAM,+EACR,IACA,CAAC;EACP;CACF;AACF;AAEA,SAAS,cAAc,MAA4B;CACjD,MAAM,SAAS,KAAK,MAAM,QAAQ;CAClC,OAAO;EACL,OAAO,kBAAkB;GACvB,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,aAAa,MAAM,EAAE;GAC/D,GAAG,WAAW,IAAI;EACpB,CAAC;EACD,OAAO;GACL,WAAW;GACX,sBAAsB;GACtB,QAAQ;EACV;CACF;AACF;AAEA,SAAS,eAAe,MAA4B;CAClD,MAAM,QAAQ,WAAW,KAAK,MAAM,cAAc,CAAC;CACnD,MAAM,YAAY,KAAK,MAAM,cAAc;CAC3C,OAAO;EACL,OAAO,mBAAmB;GACxB,WAAW,iBAAiB,KAAK,MAAM,cAAc,CAAC;GACtD,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GACzB,GAAI,cAAc,KAAA,IACd,CAAC,IACD,EAAE,WAAW,aAAa,SAAS,EAAE;GACzC,GAAG,WAAW,IAAI;EACpB,CAAC;EACD,OAAO;GACL,WAAW;GACX,sBAAsB;GACtB,QAAQ;EACV;CACF;AACF;;;;;;;;AASA,SAAgB,YACd,MACA,KACkB;CAClB,QAAQ,KAAK,SAAb;EACE,KAAK,WACH,OAAO,YAAY,MAAM,GAAG;EAC9B,KAAK,aACH,OAAO,cAAc,MAAM,GAAG;EAChC,KAAK,YACH,OAAO,aAAa,MAAM,KAAK,UAAU;EAC3C,KAAK,UACH,OAAO,aAAa,MAAM,KAAK,QAAQ;EACzC,KAAK,aACH,OAAO,cAAc,IAAI;EAC3B,KAAK,cACH,OAAO,eAAe,IAAI;EAC5B,SACE,OAAO;GACL,OAAO,gBAAgB;IACrB,SAAS,KAAK,UAAU,MAAM,MAAM,CAAC;IACrC,GAAG,WAAW,IAAI;GACpB,CAAC;GACD,OAAO;IACL,WAAW,KAAK;IAChB,sBAAsB;IACtB,QAAQ;IACR,MAAM,IAAI,KAAK,QAAQ;GACzB;EACF;CACJ;AACF;;;;;;;;ACxQA,MAAM,kBAAgD;CACpD,UAAU;CACV,SAAS;CACT,WAAW;CACX,UAAU;CACV,SAAS;CACT,QAAQ;CACR,WAAW;CACX,OAAO;CACP,UAAU;CACV,UAAU;CACV,SAAS;CACT,UAAU;CACV,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,SAAS;CACT,SAAS;AACX;AAEA,MAAM,UAA0C;CAC9C,GAAG;CACH,aAAa;AACf;AAEA,MAAM,aAA8C;CAClD,CAAC,IAAI,OAAO;CACZ,CAAC,IAAI,QAAQ;CACb,CAAC,IAAI,OAAO;AACd;AAEA,MAAM,+BAAe,IAAI,IAAY;CACnC;CACA;CACA;CACA;CACA;AACF,CAAC;AAOD,MAAM,gBAGF,EACF,YAAY;CAAE,OAAO;CAAY,MAAM;AAAO,EAChD;AAEA,SAAS,kBAAkB,KAAoC;CAC7D,MAAM,UAAU,IAAI,KAAK,CAAC,CAAC,YAAY;CACvC,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,QAAQ,UAAU,OAAO,QAAQ;CACrC,OAAO,WAAW,kBAAmB,UAA6B;AACpE;AAEA,SAAS,YAAY,IAAsD;CACzE,IAAI,OAAO,WAAW;CACtB,IAAI,MAAM;CACV,KAAK,MAAM,aAAa,YAAY;EAClC,MAAM,IAAI,KAAK,IAAI,UAAU,KAAK,EAAE;EACpC,IAAI,IAAI,KAAK;GACX,MAAM;GACN,OAAO;EACT;CACF;CACA,OAAO;EAAE,MAAM,KAAK;EAAI,OAAO,QAAQ;CAAE;AAC3C;;AAGA,SAAS,iBACP,SAC+D;CAC/D,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,MAAM,UAAU,QAAQ,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,GAAG,EAAE,KAAK;CAC3E,IAAI,aAAa,IAAI,OAAO,GAAG,OAAO,EAAE,OAAO,QAA2B;CAC1E,OAAO,cAAc;AACvB;;;;;;;;AASA,SAAgB,cACd,MACA,MACkB;CAClB,MAAM,UAAU,KAAK,MAAM,SAAS;CACpC,IAAI,CAAC,SAAS,OAAO;CAErB,MAAM,QAAkB,CAAC;CACzB,MAAM,QAAsB,CAAC;CAE7B,KAAK,MAAM,SAAS,QAAQ,KAAK,CAAC,CAAC,MAAM,KAAK,GAAG;EAC/C,MAAM,UAAU,MAAM,MAAM,GAAG,CAAC,CAAC;EACjC,IAAI,CAAC,SAAS;EAEd,MAAM,MAAM,KAAK,MAAM,GAAG,QAAQ,MAAM;EACxC,IAAI,CAAC,KAAK;EAEV,MAAM,WAAW,kBAAkB,OAAO;EAC1C,IAAI,CAAC,UACH,MAAM,KACJ,iCAAiC,QAAQ,uBAC3C;EAGF,MAAM,KAAK;GAAE,IAAI,WAAW;GAAG,UAAU,YAAY;GAAW;EAAI,CAAC;CACvE;CAEA,IAAI,MAAM,WAAW,GAAG,OAAO;CAE/B,MAAM,cAAc,iBAAiB,KAAK,MAAM,UAAU,CAAC;CAE3D,MAAM,eAAe,KAAK,MAAM,WAAW;CAC3C,IAAI;CACJ,IAAI,iBAAiB,KAAA,GAAW;EAE9B,MAAM,WAAW,YADN,aAAa,YACM,CAAC;EAC/B,WAAW,SAAS;EACpB,IAAI,CAAC,SAAS,OACZ,MAAM,KACJ,aAAa,aAAa,0CAA0C,SAAS,KAAK,GACpF;CAEJ;CAEA,OAAO;EACL,OAAO,uBAAuB;GAC5B;GACA,OAAO,eAAe,KAAK,MAAM,OAAO,GAAG,QAAQ;GACnD,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;GAC/B,GAAI,cAAc,EAAE,WAAW,YAAY,MAAM,IAAI,CAAC;GACtD,GAAI,aAAa,OAAO,EAAE,UAAU,YAAY,KAAK,IAAI,CAAC;GAC1D,GAAG,WAAW,IAAI;EACpB,CAAC;EACD,OAAO;GACL,WAAW;GACX,sBAAsB;GACtB,QAAQ,MAAM,SAAS,IAAI,iBAAiB;GAC5C,GAAI,MAAM,SAAS,IAAI,EAAE,MAAM,MAAM,KAAK,GAAG,EAAE,IAAI,CAAC;EACtD;CACF;AACF;;;ACxJA,MAAM,gBAAqE;CACzE;EAAE,QAAQ;EAAK,UAAU,CAAC,GAAG;CAAE;CAC/B;EAAE,QAAQ;EAAK,UAAU,CAAC,IAAI,EAAE;CAAE;CAClC;EAAE,QAAQ;EAAO,UAAU,CAAC,OAAO,KAAK;CAAE;CAC1C;EAAE,QAAQ;EAAO,UAAU,CAAC,OAAO,KAAK;CAAE;CAC1C;EAAE,QAAQ;EAAK,UAAU;GAAC;GAAO;GAAO;EAAK;CAAE;AACjD;AAEA,MAAM,kBAAkB;AAExB,MAAM,eAA6C;CACjD,KAAK;CACL,KAAK;CACL,KAAK;CACL,OAAO;CACP,OAAO;AACT;AAEA,SAAS,aAAa,QAAsB,WAA6B;CACvE,QAAQ,QAAR;EACE,KAAK,KACH,OAAO,CAAC,YAAY,GAAG,YAAY,CAAC;EACtC,KAAK,KACH,OAAO;GAAC,YAAY;GAAG,YAAY;GAAG,YAAY;EAAC;EACrD,KAAK,OACH,OAAO,CAAC,YAAY,GAAI,YAAY,IAAK,CAAC;EAC5C,KAAK,OACH,OAAO,CAAE,YAAY,IAAK,GAAG,YAAY,CAAC;EAC5C,SACE,OAAO,CAAC,SAAS;CACrB;AACF;;;;;;;;AASA,SAAgB,kBAAkB,UAGhC;CACA,MAAM,QAAQ,SAAS;CACvB,IAAI,UAAU,GAAG,OAAO;EAAE,QAAQ;EAAK,OAAO;CAAK;CAInD,IAAI,SAAS,OAAO,MAAM,MAAM,IAAI,GAAG;EACrC,IAAI,UAAU,GAAG,OAAO;GAAE,QAAQ;GAAK,OAAO;EAAK;EACnD,IAAI,UAAU,GAAG,OAAO;GAAE,QAAQ;GAAK,OAAO;EAAK;EACnD,IAAI,UAAU,GAAG,OAAO;GAAE,QAAQ;GAAK,OAAO;EAAK;EACnD,OAAO;GAAE,QAAQ;GAAK,OAAO;EAAM;CACrC;CAEA,MAAM,QAAQ,SAAS,QAAQ,MAAmB,MAAM,IAAI;CAC5D,MAAM,aAAa,QAAQ,MAAM;CACjC,MAAM,YACJ,eAAe,IACX,IACA,KAAK,IAAI,GAAG,MAAM,MAAM,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI;CAC5D,MAAM,WAAW,SAAS,KAAK,MAAM,KAAK,SAAS;CAEnD,MAAM,YAAY,cAAc,QAAQ,MAAM,EAAE,SAAS,WAAW,KAAK;CAEzE,KAAK,MAAM,SAAS,WAClB,IACE,MAAM,SAAS,OACZ,MAAM,MAAM,KAAK,IAAI,OAAO,SAAS,EAAE,KAAK,eAC/C,GAEA,OAAO;EAAE,QAAQ,MAAM;EAAQ,OAAO;CAAK;CAI/C,MAAM,aACJ,UAAU,SAAS,IACf,YACA,cAAc,QAAQ,MAAM,EAAE,WAAW,GAAG;CAElD,IAAI,OAAO,WAAW;CACtB,IAAI,YAAY;CAChB,KAAK,MAAM,SAAS,YAAY;EAC9B,MAAM,QAAQ,MAAM,SAAS,QAC1B,KAAK,MAAM,MAAM,MAAM,KAAK,IAAI,QAAQ,SAAS,MAAM,EAAE,GAC1D,CACF;EACA,IAAI,QAAQ,WAAW;GACrB,YAAY;GACZ,OAAO;EACT;CACF;CAEA,OAAO;EAAE,QAAQ,KAAK;EAAQ,OAAO;CAAM;AAC7C;AAEA,SAAS,sBACP,QACA,KACA,SACS;CACT,MAAM,SAAkB,CAAC;CACzB,KAAK,MAAM,SAAS,OAAO,YAAY,CAAC,GAAG;EACzC,MAAM,YACJ,MAAM,YAAY,cACd,cAAc,OAAO,GAAG,IACxB,YAAY,OAAO,GAAG;EAC5B,IAAI,CAAC,WAAW;EAChB,QAAQ,KAAK,UAAU,KAAK;EAC5B,IAAI,UAAU,OAAO,OAAO,KAAK,UAAU,KAAK;CAClD;CACA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,aACd,MACA,KACA,SACS;CACT,MAAM,WAAW,KAAK,YAAY,CAAC,EAAA,CAAG,QACnC,MAAM,EAAE,YAAY,WACvB;CACA,IAAI,QAAQ,WAAW,GAAG,OAAO,CAAC;CAElC,MAAM,YAAY,QAAQ,KAAK,MAAM,KAAK,GAAG,OAAO,CAAC;CAErD,MAAM,EAAE,QAAQ,UAAU,kBADT,UAAU,KAAK,MAAM,aAAa,CAAC,CACD,CAAC;CAEpD,MAAM,QAAQ,aAAa;CAC3B,MAAM,SAAS,aAAa,QAAQ,IAAI,MAAM,SAAS,KAAK;CAC5D,MAAM,WAAsB,MAAM,KAAK,EAAE,QAAQ,MAAM,SAAS,CAAC,CAAC;CAElE,MAAM,aAAa,yBAChB,SAAS,KAAK,MAAM,IAAI,GACzB,WACA,IAAI,MAAM,SAAS,KACrB;CACA,MAAM,QAAQ,CACZ,GAAI,QACA,CAAC,IACD,CACE,iBAAiB,UAAU,KAAK,MAAM,KAAK,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,kDAAkD,OAAO,GACzH,GACJ,GAAI,YAAY,SAAS,CAAC,CAC5B;CACA,QAAQ,KAAK;EACX,WAAW;EACX,sBAAsB;EACtB,QAAQ,MAAM,SAAS,IAAI,iBAAiB;EAC5C,GAAI,MAAM,SAAS,IAAI,EAAE,MAAM,MAAM,KAAK,GAAG,EAAE,IAAI,CAAC;CACtD,CAAC;CAED,QAAQ,SAAS,QAAQ,UAAU;EACjC,MAAM,OAAO,KAAK,IAAI,OAAO,QAAQ,CAAC;EACtC,MAAM,YAAwB;GAC5B,GAAG;GACH,aAAa,KAAK,MAAM,OAAO,SAAS,IAAI,MAAM,SAAS,KAAK;EAClE;EACA,SAAS,KAAK,CAAC,KAAK,GAAG,sBAAsB,QAAQ,WAAW,OAAO,CAAC;CAC1E,CAAC;CAED,MAAM,kBAAkB,WAAW,KAAK,MAAM,kBAAkB,CAAC;CAEjE,OAAO,CACL,mBAAmB;EACjB,SAAS;EACT;EACA,QAAQ;GACN,SAAS,aAAa,IAAI;GAC1B,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;EAC/C;EACA,GAAI,aAAa,EAAE,iBAAiB,WAAW,MAAM,IAAI,CAAC;CAC5D,CAAC,CACH;AACF;;;ACxLA,MAAM,WAAW;AAEjB,MAAM,yBACJ;AAEF,MAAM,uBACJ;;;;;;;;;;;;;;;;;;;;AAqBF,SAAgB,qBACd,QACc;CACd,MAAM,OAAgB,OAAO,WAAW,WAAW,UAAU,MAAM,IAAI;CAEvE,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GACjE,MAAM,IAAI,MAAM,sBAAsB;CAGxC,MAAM,cAAc;CAEpB,IAAI,YAAY,YAAY,UAAU;EAOpC,IAAI,UAAU,eAAe,gBAAgB,aAC3C,MAAM,IAAI,MAAM,sBAAsB;EAExC,MAAM,IAAI,MACR,8DAA8D,SAAS,GACzE;CACF;CAEA,MAAM,aAAa,YAAY,YAAY,CAAC,EAAA,CAAG,MAC5C,UAAU,MAAM,YAAY,cAC/B;CAEA,MAAM,UAA+B,CAAC;CACtC,MAAM,WAAqB,CAAC;CAC5B,MAAM,SAAkB,CAAC;CAEzB,MAAM,QAAQ,gBAAgB,aAAa,WAAW,QAAQ;CAE9D,MAAM,MAAkB;EACtB;EACA,aAAa,MAAM,SAAS;CAC9B;CAEA,KAAK,MAAM,SAAS,WAAW,YAAY,CAAC,GAAG;EAC7C,IAAI,MAAM,YAAY,cAAc;EACpC,OAAO,KAAK,GAAG,aAAa,OAAO,KAAK,OAAO,CAAC;CAClD;CAEA,IAAI,OAAO,WAAW,GACpB,SAAS,KAAK,oBAAoB;CAmBpC,OAAO;EAAE,SAAA;GAfP,GAAG,6BAA6B;GAChC;GACA,UAAU,MAAM;EAaH;EAAG,QAAA;GAFa;GAAS;GAAU,SAAA;IAPhD,OAAO,QAAQ;IACf,WAAW,QAAQ,QAAQ,MAAM,EAAE,WAAW,WAAW,CAAC,CAAC;IAC3D,cAAc,QAAQ,QAAQ,MAAM,EAAE,WAAW,cAAc,CAAC,CAAC;IACjE,cAAc,QAAQ,QAAQ,MAAM,EAAE,WAAW,eAAe,CAAC,CAAC;IAClE,SAAS,QAAQ,QAAQ,MAAM,EAAE,WAAW,SAAS,CAAC,CAAC;GAGD;EAEjC;CAAE;AAC3B;AAEA,SAAS,UAAU,QAAyB;CAC1C,IAAI;EACF,OAAO,KAAK,MAAM,MAAM;CAC1B,QAAQ;EACN,MAAM,IAAI,MAAM,sBAAsB;CACxC;AACF"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@templatical/import-topol",
|
|
3
3
|
"description": "Convert Topol.io email templates to Templatical format",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.48.0",
|
|
5
5
|
"bugs": "https://github.com/templatical/sdk/issues",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@templatical/types": "0.
|
|
7
|
+
"@templatical/types": "0.48.0"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"typescript": "^6.0.3",
|
|
11
|
-
"vitest": "^5.0.
|
|
11
|
+
"vitest": "^5.0.4"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|