@templatical/import-mjml 0.44.3 → 0.46.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { load } from "cheerio";
2
- import { DEFAULT_TEMPLATE_DEFAULTS, RICH_TEXT_SPACING, SYNTAX_PRESETS, createButtonBlock, createDefaultTemplateContent, createDividerBlock, createHtmlBlock, createImageBlock, createMenuBlock, createParagraphBlock, createSectionBlock, createSocialIconsBlock, createSpacerBlock, createTableBlock, createTitleBlock, generateId, resolveContentDirection } from "@templatical/types";
2
+ import { BORDER_SIDES, DEFAULT_TEMPLATE_DEFAULTS, RICH_TEXT_SPACING, SOCIAL_ICON_STYLES, SOCIAL_ICON_TONES, SYNTAX_PRESETS, canonicalFontStack, createButtonBlock, createDefaultTemplateContent, createDividerBlock, createHtmlBlock, createImageBlock, createMenuBlock, createParagraphBlock, createSectionBlock, createSocialIconsBlock, createSpacerBlock, createTableBlock, createTitleBlock, generateId, resolveContentDirection } from "@templatical/types";
3
3
  import { isTag, isText } from "domhandler";
4
4
  //#region src/attribute-resolver.ts
5
5
  /**
@@ -137,9 +137,9 @@ const HIDE_MOBILE = "tpl-hide-mobile";
137
137
  /**
138
138
  * Marks a rendered title or paragraph's rich-text spacing — mirrors
139
139
  * `RICH_TEXT_CSS_CLASS` in `packages/renderer/src/rich-text.ts`. `title.ts`
140
- * and `paragraph.ts` are the only two renderers that pass a second argument
141
- * to `getCssClassAttr`, so every rendered title and paragraph carries this
142
- * class on `css-class` alongside any visibility markers.
140
+ * and `paragraph.ts` pass it as a second argument to `getCssClassAttr`, so
141
+ * every rendered title and paragraph carries this class on `css-class`
142
+ * alongside any visibility markers.
143
143
  */
144
144
  const RICH_TEXT_CSS_CLASS = "tpl-rich-text";
145
145
  /**
@@ -156,14 +156,21 @@ const RICH_TEXT_CSS_CLASS = "tpl-rich-text";
156
156
  * about what a gap class looks like.
157
157
  */
158
158
  const RICH_TEXT_GAP_CLASS = /^tpl-rich-text-(\d+(?:\.\d+)?)$/;
159
+ /**
160
+ * Marks a rendered full-width button — mirrors `BUTTON_FULL_WIDTH_CSS_CLASS`
161
+ * in `packages/renderer/src/renderers/button.ts`. It carries no meaning of its
162
+ * own: `width="100%"` on the same `mj-button` already says the button is
163
+ * full-width.
164
+ */
165
+ const BUTTON_FULL_WIDTH_CSS_CLASS = "tpl-button-full";
159
166
  function cssClasses(attrs) {
160
167
  return (attrs["css-class"] ?? "").trim().split(/\s+/).filter(Boolean);
161
168
  }
162
169
  /**
163
170
  * Reverse of the renderer's `getCssClassAttr` (`packages/renderer/src/visibility.ts`).
164
- * `css-class` also carries the rich-text markers below (`RICH_TEXT_CSS_CLASS`,
165
- * `RICH_TEXT_GAP_CLASS`) — this function reads only the two visibility
166
- * classes off it and leaves those alone.
171
+ * `css-class` also carries the renderer's other markers (`RICH_TEXT_CSS_CLASS`,
172
+ * `RICH_TEXT_GAP_CLASS`, `BUTTON_FULL_WIDTH_CSS_CLASS`) — this function reads
173
+ * only the two visibility classes off it and leaves those alone.
167
174
  *
168
175
  * Returns `undefined` when neither class is present — absence means visible
169
176
  * everywhere, and writing `{ desktop: true, mobile: true }` instead would put a
@@ -200,13 +207,14 @@ function readParagraphGap(attrs) {
200
207
  * home in the block model, and a template that relies on them will render
201
208
  * differently after import.
202
209
  *
203
- * Excludes the renderer's own rich-text markers alongside the two visibility
204
- * classes, so importing a template the renderer itself produced does not
205
- * report a title or paragraph's own spacing class as foreign. A consumer's
206
- * own `tpl-`-prefixed class is not one of these markers and is still reported.
210
+ * Excludes the renderer's own rich-text and full-width button markers
211
+ * alongside the two visibility classes, so importing a template the renderer
212
+ * itself produced does not report one of its own classes as foreign. A
213
+ * consumer's own `tpl-`-prefixed class is not one of these markers and is
214
+ * still reported.
207
215
  */
208
216
  function readForeignCssClasses(attrs) {
209
- return cssClasses(attrs).filter((name) => name !== HIDE_DESKTOP && name !== HIDE_MOBILE && name !== RICH_TEXT_CSS_CLASS && !RICH_TEXT_GAP_CLASS.test(name));
217
+ return cssClasses(attrs).filter((name) => name !== HIDE_DESKTOP && name !== HIDE_MOBILE && name !== RICH_TEXT_CSS_CLASS && name !== BUTTON_FULL_WIDTH_CSS_CLASS && !RICH_TEXT_GAP_CLASS.test(name));
210
218
  }
211
219
  //#endregion
212
220
  //#region src/attribute-parser.ts
@@ -313,13 +321,6 @@ function parsePaddingShorthand(value) {
313
321
  }
314
322
  }
315
323
  /**
316
- * Strips quotes and returns the first font in a font-family stack.
317
- */
318
- function parseFontFamily(value) {
319
- if (!value) return "";
320
- return value.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
321
- }
322
- /**
323
324
  * Parses an alignment to one of the three the block model accepts.
324
325
  */
325
326
  function parseAlignment(value, fallback = "left") {
@@ -353,6 +354,114 @@ function parseDefinitePx(value) {
353
354
  const match = value.trim().match(/^(-?\d+(?:\.\d+)?)\s*(?:px)?$/);
354
355
  return match ? parseFloat(match[1]) : null;
355
356
  }
357
+ const PX_LENGTH = /^(\d+(?:\.\d+)?)(?:px)?$/i;
358
+ /**
359
+ * Reads a `border-radius` value: one px length, or the two- to four-value
360
+ * shorthand (top-left, top-right, bottom-right, bottom-left), expanded the way
361
+ * CSS expands it. Four matching corners come back as one number, the form the
362
+ * renderer writes them in. `null` when any part is not a non-negative px
363
+ * length (`50%`, `1em`, an elliptical `8px / 4px`), which CSS either reads
364
+ * against the box or discards.
365
+ */
366
+ function parseBorderRadius(value) {
367
+ const parts = value.trim().split(/\s+/);
368
+ if (parts.length > 4) return null;
369
+ const px = [];
370
+ for (const part of parts) {
371
+ const match = PX_LENGTH.exec(part);
372
+ if (!match) return null;
373
+ px.push(Math.round(parseFloat(match[1])));
374
+ }
375
+ const [topLeft, topRight = topLeft, bottomRight = topLeft, bottomLeft = topRight] = px;
376
+ if (topRight === topLeft && bottomRight === topLeft && bottomLeft === topLeft) return topLeft;
377
+ return {
378
+ topLeft,
379
+ topRight,
380
+ bottomRight,
381
+ bottomLeft
382
+ };
383
+ }
384
+ const DRAWN_STYLES = /* @__PURE__ */ new Set([
385
+ "solid",
386
+ "dashed",
387
+ "dotted"
388
+ ]);
389
+ const NARROWED_STYLES = /* @__PURE__ */ new Set([
390
+ "double",
391
+ "groove",
392
+ "ridge",
393
+ "inset",
394
+ "outset"
395
+ ]);
396
+ const UNDRAWN_STYLES = /* @__PURE__ */ new Set(["none", "hidden"]);
397
+ /**
398
+ * Splits a CSS value on whitespace outside parentheses, so `rgb(0, 0, 0)` stays
399
+ * one token. A linear scan rather than a regex, which would backtrack on a long
400
+ * run of spaces.
401
+ */
402
+ function splitCssTokens(value) {
403
+ const tokens = [];
404
+ let current = "";
405
+ let depth = 0;
406
+ for (const ch of value) {
407
+ if (ch === "(") depth++;
408
+ if (ch === ")") depth = Math.max(0, depth - 1);
409
+ if (depth === 0 && /\s/.test(ch)) {
410
+ if (current) tokens.push(current);
411
+ current = "";
412
+ continue;
413
+ }
414
+ current += ch;
415
+ }
416
+ if (current) tokens.push(current);
417
+ return tokens;
418
+ }
419
+ /**
420
+ * Reads one CSS `border` declaration, its width, style and colour in any
421
+ * order, as a `BorderSideValue`.
422
+ *
423
+ * `side` is `null` when CSS draws nothing: style `none` or `hidden`, no style
424
+ * at all (CSS's initial style is `none`), or a `0` width. A style the block
425
+ * model lacks narrows to solid and is named in `narrowedFrom`. A missing or
426
+ * unknown colour is `""`, which the renderer writes back as no colour, so CSS
427
+ * draws `currentColor` both times.
428
+ *
429
+ * Returns `null` for a drawn border it cannot hold: a width that is not px
430
+ * (`2em`, `thin`), no width at all (CSS draws `medium`), or a value with two
431
+ * of any part.
432
+ */
433
+ function parseBorderDeclaration(value) {
434
+ let width;
435
+ let style;
436
+ let color;
437
+ for (const token of splitCssTokens(value.trim())) {
438
+ const lower = token.toLowerCase();
439
+ const px = PX_LENGTH.exec(lower);
440
+ const isStyle = DRAWN_STYLES.has(lower) || NARROWED_STYLES.has(lower) || UNDRAWN_STYLES.has(lower);
441
+ if (px) {
442
+ if (width !== void 0) return null;
443
+ width = Math.round(parseFloat(px[1]));
444
+ } else if (isStyle) {
445
+ if (style !== void 0) return null;
446
+ style = lower;
447
+ } else if (/^[-+.\d]/.test(lower) || /^(thin|medium|thick)$/.test(lower)) return null;
448
+ else {
449
+ if (color !== void 0) return null;
450
+ color = token;
451
+ }
452
+ }
453
+ if (style === void 0 || UNDRAWN_STYLES.has(style) || width === 0) return { side: null };
454
+ if (width === void 0) return null;
455
+ const side = {
456
+ width,
457
+ style: NARROWED_STYLES.has(style) ? "solid" : style,
458
+ color: parseColor(color)
459
+ };
460
+ return NARROWED_STYLES.has(style) ? {
461
+ side,
462
+ narrowedFrom: style
463
+ } : { side };
464
+ }
356
465
  /**
357
466
  * Narrows a border style to the three `DividerBlock.lineStyle` accepts.
358
467
  */
@@ -391,6 +500,65 @@ function baseFields(attrs, placement = "container") {
391
500
  ...visibility ? { visibility } : {}
392
501
  };
393
502
  }
503
+ /**
504
+ * `border-radius` as a `BorderRadiusValue`, `undefined` when the attribute is
505
+ * absent. A value with no px reading is square corners, `0`, and pushes a
506
+ * note: `label` names the element in it.
507
+ */
508
+ function readBorderRadius(attrs, label, notes) {
509
+ const raw = attrs["border-radius"];
510
+ if (raw === void 0) return void 0;
511
+ const radius = parseBorderRadius(raw);
512
+ if (radius === null) {
513
+ notes.push(`${label} border-radius "${raw.trim()}" could not be read; imported as square corners.`);
514
+ return 0;
515
+ }
516
+ return radius;
517
+ }
518
+ /**
519
+ * What the editor's border control holds for a side with no width
520
+ * (`BorderControl.vue`), so a border drawn on some sides reads back the way
521
+ * the editor would have stored it.
522
+ */
523
+ const UNDRAWN_SIDE = {
524
+ width: 0,
525
+ style: "solid",
526
+ color: "#000000"
527
+ };
528
+ /**
529
+ * `border` and `border-<side>` as a `BorderValue`, `undefined` when no side is
530
+ * drawn. A side takes its own `border-<side>` over `border`, since MJML writes
531
+ * the side after the shorthand. A declaration that a side actually uses pushes
532
+ * a note when its style narrows to solid or it cannot be read, in which case
533
+ * that side is left undrawn.
534
+ */
535
+ function readBorder(attrs, label, notes) {
536
+ const read = /* @__PURE__ */ new Map();
537
+ const sides = {};
538
+ for (const side of BORDER_SIDES) {
539
+ const name = attrs[`border-${side}`] !== void 0 ? `border-${side}` : attrs.border !== void 0 ? "border" : null;
540
+ if (name === null) {
541
+ sides[side] = null;
542
+ continue;
543
+ }
544
+ if (!read.has(name)) {
545
+ const raw = attrs[name].trim();
546
+ const parsed = parseBorderDeclaration(raw);
547
+ if (parsed === null) notes.push(`${label} ${name} "${raw}" could not be read; imported without it.`);
548
+ else if (parsed.narrowedFrom) notes.push(`${label} ${name} "${raw}" uses style ${parsed.narrowedFrom}; imported as solid.`);
549
+ read.set(name, parsed?.side ?? null);
550
+ }
551
+ const parsed = read.get(name);
552
+ sides[side] = parsed && { ...parsed };
553
+ }
554
+ if (BORDER_SIDES.every((side) => sides[side] === null)) return void 0;
555
+ return {
556
+ top: sides.top ?? { ...UNDRAWN_SIDE },
557
+ right: sides.right ?? { ...UNDRAWN_SIDE },
558
+ bottom: sides.bottom ?? { ...UNDRAWN_SIDE },
559
+ left: sides.left ?? { ...UNDRAWN_SIDE }
560
+ };
561
+ }
394
562
  function warnForeignClasses(attrs, tag, ctx) {
395
563
  for (const name of readForeignCssClasses(attrs)) ctx.warnings.push(`Dropped CSS class "${name}" on <${tag}> — consumer CSS has no Templatical equivalent.`);
396
564
  }
@@ -468,7 +636,7 @@ function normalizePlatform(raw) {
468
636
  }
469
637
  return null;
470
638
  }
471
- /** The `<style>/<platform>.png` tail of the URL `renderers/social.ts:76` builds. */
639
+ /** The `<dir>/<platform>.png` tail of the URL `renderers/social.ts:79` builds. */
472
640
  function platformFromSrc(src) {
473
641
  const parts = src.split("?")[0].split("/").filter(Boolean);
474
642
  return {
@@ -502,19 +670,26 @@ const RADIUS_STYLES = {
502
670
  "0": "square",
503
671
  "4px": "solid"
504
672
  };
505
- const KNOWN_ICON_STYLES = /* @__PURE__ */ new Set([
506
- "solid",
507
- "outlined",
508
- "rounded",
509
- "square",
510
- "circle"
511
- ]);
673
+ /**
674
+ * The style and tone in an icon folder the renderer names: `circle` for a
675
+ * brand-colored circle, `circle-dark` for a dark one (`socialIconAssetDir`).
676
+ */
677
+ function iconDirParts(dir) {
678
+ const [style, tone, ...rest] = dir.split("-");
679
+ if (rest.length > 0 || !SOCIAL_ICON_STYLES.includes(style)) return null;
680
+ if (tone === void 0) return { style };
681
+ return SOCIAL_ICON_TONES.includes(tone) && tone !== "brand" ? {
682
+ style,
683
+ tone
684
+ } : null;
685
+ }
512
686
  function convertSocial($el, attrs, ctx) {
513
687
  const elements = childElements($el, ctx.$).filter(($child) => tagOf($child[0]) === "mj-social-element");
514
688
  if (elements.length === 0) return null;
515
689
  const notes = [];
516
690
  const icons = [];
517
691
  let iconStyle = null;
692
+ let iconTone;
518
693
  let iconSizePx = 0;
519
694
  let spacing = 0;
520
695
  elements.forEach(($child, index) => {
@@ -536,7 +711,11 @@ function convertSocial($el, attrs, ctx) {
536
711
  platform: platform ?? "website",
537
712
  url: (childAttrs.href ?? "").trim()
538
713
  });
539
- if (!iconStyle && KNOWN_ICON_STYLES.has(fromSrc.style)) iconStyle = fromSrc.style;
714
+ const fromDir = iconStyle ? null : iconDirParts(fromSrc.style);
715
+ if (fromDir) {
716
+ iconStyle = fromDir.style;
717
+ iconTone = fromDir.tone;
718
+ }
540
719
  if (iconSizePx === 0) iconSizePx = parsePxValue(childAttrs["icon-size"]);
541
720
  if (spacing === 0 && index < elements.length - 1) spacing = parsePxValue((childAttrs.padding ?? "").trim().split(/\s+/)[1]);
542
721
  if (!iconStyle) {
@@ -561,6 +740,7 @@ function convertSocial($el, attrs, ctx) {
561
740
  align: parseAlignment(attrs.align, "center"),
562
741
  ...iconSize ? { iconSize } : {},
563
742
  ...iconStyle ? { iconStyle } : {},
743
+ ...iconTone ? { iconTone } : {},
564
744
  ...spacing > 0 ? { spacing } : {},
565
745
  ...baseFields(attrs)
566
746
  }),
@@ -589,7 +769,7 @@ function convertNavbar($el, attrs, ctx) {
589
769
  };
590
770
  });
591
771
  const fontSize = parsePxValue(attrs["font-size"]);
592
- const fontFamily = parseFontFamily(ownAttr(attrs, "font-family", "mj-navbar", ctx.cascade));
772
+ const fontFamily = canonicalFontStack(ownAttr(attrs, "font-family", "mj-navbar", ctx.cascade));
593
773
  return {
594
774
  block: createMenuBlock({
595
775
  items,
@@ -619,7 +799,7 @@ function convertNativeTable($el, attrs, ctx) {
619
799
  const hasHeaderRow = $(rowEls[0]).children().toArray().some((cell) => tagOf(cell) === "th");
620
800
  const color = parseColor(ownAttr(attrs, "color", "mj-table", ctx.cascade));
621
801
  const fontSize = parsePxValue(attrs["font-size"]);
622
- const fontFamily = parseFontFamily(ownAttr(attrs, "font-family", "mj-table", ctx.cascade));
802
+ const fontFamily = canonicalFontStack(ownAttr(attrs, "font-family", "mj-table", ctx.cascade));
623
803
  return {
624
804
  block: createTableBlock({
625
805
  rows,
@@ -674,7 +854,7 @@ function convertTitle(html, attrs, sourceLevel, cascade) {
674
854
  const $heading = parseInner(html)("body").children().first();
675
855
  const level = Math.min(sourceLevel, 4);
676
856
  const color = parseColor(ownAttr(attrs, "color", "mj-text", cascade));
677
- const fontFamily = parseFontFamily(ownAttr(attrs, "font-family", "mj-text", cascade));
857
+ const fontFamily = canonicalFontStack(ownAttr(attrs, "font-family", "mj-text", cascade));
678
858
  const block = createTitleBlock({
679
859
  content: $heading.html() ?? "",
680
860
  level,
@@ -707,7 +887,7 @@ function convertTable(html, attrs, cascade) {
707
887
  const hasHeaderRow = $rows.length > 0 && $inner($rows[0]).children("th").length > 0;
708
888
  const color = parseColor(ownAttr(attrs, "color", "mj-text", cascade));
709
889
  const fontSize = parsePxValue(attrs["font-size"]);
710
- const fontFamily = parseFontFamily(ownAttr(attrs, "font-family", "mj-text", cascade));
890
+ const fontFamily = canonicalFontStack(ownAttr(attrs, "font-family", "mj-text", cascade));
711
891
  return {
712
892
  block: createTableBlock({
713
893
  rows,
@@ -772,7 +952,7 @@ function convertMenu(html, attrs, cascade) {
772
952
  const spacing = parsePxValue($separator.attr("style")?.match(/padding\s*:\s*0\s+([\d.]+px)/i)?.[1]);
773
953
  const color = parseColor(ownAttr(attrs, "color", "mj-text", cascade));
774
954
  const fontSize = parsePxValue(attrs["font-size"]);
775
- const fontFamily = parseFontFamily(ownAttr(attrs, "font-family", "mj-text", cascade));
955
+ const fontFamily = canonicalFontStack(ownAttr(attrs, "font-family", "mj-text", cascade));
776
956
  return {
777
957
  block: createMenuBlock({
778
958
  items,
@@ -965,39 +1145,105 @@ function convertImage($el, attrs, ctx) {
965
1145
  const decorative = (attrs.role ?? "").trim().toLowerCase() === "presentation";
966
1146
  const pxWidth = parsePxValue(attrs.width);
967
1147
  const height = parsePxValue(attrs.height);
968
- const borderRadius = parsePxValue(attrs["border-radius"]);
969
1148
  const href = (attrs.href ?? "").trim();
970
- return createImageBlock({
971
- src,
972
- alt: decorative ? "" : attrs.alt ?? "",
973
- width: pxWidth === ctx.containerWidth ? "full" : pxWidth || "full",
974
- align: parseAlignment(attrs.align, "center"),
975
- ...height > 0 ? { height } : {},
976
- ...borderRadius > 0 ? { borderRadius } : {},
977
- ...href ? { linkUrl: href } : {},
978
- ...href && isNewTab(attrs) ? { linkOpenInNewTab: true } : {},
979
- ...decorative ? { decorative: true } : {},
980
- ...baseFields(attrs)
981
- });
1149
+ const notes = [];
1150
+ const borderRadius = readBorderRadius(attrs, "Image", notes);
1151
+ const border = readBorder(attrs, "Image", notes);
1152
+ return {
1153
+ block: createImageBlock({
1154
+ src,
1155
+ alt: decorative ? "" : attrs.alt ?? "",
1156
+ width: pxWidth === ctx.containerWidth ? "full" : pxWidth || "full",
1157
+ align: parseAlignment(attrs.align, "center"),
1158
+ ...height > 0 ? { height } : {},
1159
+ ...borderRadius ? { borderRadius } : {},
1160
+ ...border ? { border } : {},
1161
+ ...href ? { linkUrl: href } : {},
1162
+ ...href && isNewTab(attrs) ? { linkOpenInNewTab: true } : {},
1163
+ ...decorative ? { decorative: true } : {},
1164
+ ...baseFields(attrs)
1165
+ }),
1166
+ entry: {
1167
+ sourceTag: "mj-image",
1168
+ templaticalBlockType: "image",
1169
+ status: notes.length > 0 ? "approximated" : "converted",
1170
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
1171
+ }
1172
+ };
1173
+ }
1174
+ /**
1175
+ * `mj-button`'s `width` as `ButtonBlock.width`, where no width is a button
1176
+ * that fits its label. `100%` is `"full"` and a px width stays px, as the
1177
+ * renderer writes them. Any other percentage becomes px of `fillWidth`, the
1178
+ * box MJML sizes it against, since the block holds no percentage. A width
1179
+ * with no size fits the label, which is what MJML draws for it. Anything but
1180
+ * `100%` or a positive px width pushes a note.
1181
+ */
1182
+ function readButtonWidth(value, fillWidth, notes) {
1183
+ const raw = (value ?? "").trim();
1184
+ if (!raw) return void 0;
1185
+ const match = /^(-?\d+(?:\.\d+)?)\s*(px|%)?$/i.exec(raw);
1186
+ if (!match) {
1187
+ notes.push(`Button width "${raw}" could not be read; imported as fit to content.`);
1188
+ return;
1189
+ }
1190
+ const parsed = parseFloat(match[1]);
1191
+ const isPercent = match[2] === "%";
1192
+ if (isPercent && parsed >= 100) {
1193
+ if (parsed > 100) notes.push(`Button width ${raw} was clamped to 100%.`);
1194
+ return "full";
1195
+ }
1196
+ const px = Math.round(isPercent ? parsed / 100 * fillWidth : parsed);
1197
+ if (px <= 0) {
1198
+ notes.push(`Button width ${raw} has no usable size; imported as fit to content.`);
1199
+ return;
1200
+ }
1201
+ if (isPercent) notes.push(`Button width ${raw} was imported as ${px}px, which does not scale with its column.`);
1202
+ return px;
982
1203
  }
983
- function convertButton($el, attrs) {
1204
+ /**
1205
+ * A percentage `width` sizes the button against its column less the column's
1206
+ * side padding and its own, the same box an `mj-divider` line spans.
1207
+ *
1208
+ * `font-family` is read only when the button sets its own: the renderer
1209
+ * writes the document font onto `mj-all`, which reaches every button.
1210
+ */
1211
+ function convertButton($el, attrs, ctx) {
984
1212
  const text = ($el.text() ?? "").trim();
985
1213
  if (!text) return null;
986
1214
  const backgroundColor = parseColor(attrs["background-color"]);
987
1215
  const textColor = parseColor(attrs.color);
988
1216
  const fontSize = parsePxValue(attrs["font-size"]);
989
- return createButtonBlock({
990
- text,
991
- url: (attrs.href ?? "").trim(),
992
- ...backgroundColor ? { backgroundColor } : {},
993
- ...textColor ? { textColor } : {},
994
- ...fontSize > 0 ? { fontSize } : {},
995
- ...attrs["border-radius"] !== void 0 ? { borderRadius: parsePxValue(attrs["border-radius"]) } : {},
996
- ...attrs["inner-padding"] !== void 0 ? { buttonPadding: parsePaddingShorthand(attrs["inner-padding"]) } : {},
997
- align: parseAlignment(attrs.align, "center"),
998
- ...isNewTab(attrs) ? { openInNewTab: true } : {},
999
- ...baseFields(attrs)
1000
- });
1217
+ const fontFamily = canonicalFontStack(ownAttr(attrs, "font-family", "mj-button", ctx.cascade));
1218
+ const base = baseFields(attrs);
1219
+ const { left, right } = base.styles.padding;
1220
+ const notes = [];
1221
+ const width = readButtonWidth(attrs.width, ctx.containerWidth - ctx.columnPadding - left - right, notes);
1222
+ const borderRadius = readBorderRadius(attrs, "Button", notes);
1223
+ const border = readBorder(attrs, "Button", notes);
1224
+ return {
1225
+ block: createButtonBlock({
1226
+ text,
1227
+ url: (attrs.href ?? "").trim(),
1228
+ ...backgroundColor ? { backgroundColor } : {},
1229
+ ...textColor ? { textColor } : {},
1230
+ ...fontSize > 0 ? { fontSize } : {},
1231
+ ...fontFamily ? { fontFamily } : {},
1232
+ ...borderRadius !== void 0 ? { borderRadius } : {},
1233
+ ...border ? { border } : {},
1234
+ ...attrs["inner-padding"] !== void 0 ? { buttonPadding: parsePaddingShorthand(attrs["inner-padding"]) } : {},
1235
+ ...width !== void 0 ? { width } : {},
1236
+ align: parseAlignment(attrs.align, "center"),
1237
+ ...isNewTab(attrs) ? { openInNewTab: true } : {},
1238
+ ...base
1239
+ }),
1240
+ entry: {
1241
+ sourceTag: "mj-button",
1242
+ templaticalBlockType: "button",
1243
+ status: notes.length > 0 ? "approximated" : "converted",
1244
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
1245
+ }
1246
+ };
1001
1247
  }
1002
1248
  /**
1003
1249
  * `mj-divider`'s `width` as `DividerBlock.width`. A percentage is a share of
@@ -1091,30 +1337,8 @@ function convertElement($el, ctx) {
1091
1337
  if (tag === "mj-social") return convertSocial($el, attrs, ctx);
1092
1338
  if (tag === "mj-navbar") return convertNavbar($el, attrs, ctx);
1093
1339
  if (tag === "mj-table") return convertNativeTable($el, attrs, ctx);
1094
- if (tag === "mj-image") {
1095
- const block = convertImage($el, attrs, ctx);
1096
- if (!block) return null;
1097
- return {
1098
- block,
1099
- entry: {
1100
- sourceTag: tag,
1101
- templaticalBlockType: "image",
1102
- status: "converted"
1103
- }
1104
- };
1105
- }
1106
- if (tag === "mj-button") {
1107
- const block = convertButton($el, attrs);
1108
- if (!block) return null;
1109
- return {
1110
- block,
1111
- entry: {
1112
- sourceTag: tag,
1113
- templaticalBlockType: "button",
1114
- status: "converted"
1115
- }
1116
- };
1117
- }
1340
+ if (tag === "mj-image") return convertImage($el, attrs, ctx);
1341
+ if (tag === "mj-button") return convertButton($el, attrs, ctx);
1118
1342
  if (tag === "mj-divider") return convertDivider(attrs, ctx);
1119
1343
  if (tag === "mj-spacer") return {
1120
1344
  block: convertSpacer(attrs),
@@ -1286,7 +1510,7 @@ function extractSettings($, cascade, warnings) {
1286
1510
  const $root = findByTag($, "mjml").first();
1287
1511
  const width = parsePxValue($body.attr("width")) || REQUIRED_TEMPLATE_DEFAULTS.width;
1288
1512
  const backgroundColor = parseColor($body.attr("background-color")) || REQUIRED_TEMPLATE_DEFAULTS.backgroundColor;
1289
- const fontFromCascade = parseFontFamily(cascade.all["font-family"]) || parseFontFamily(cascade.byTag["mj-text"]?.["font-family"]);
1513
+ const fontFromCascade = canonicalFontStack(cascade.all["font-family"]) || canonicalFontStack(cascade.byTag["mj-text"]?.["font-family"]);
1290
1514
  const fontFromDeclaration = findByTag($, "mj-font").first().attr("name") ?? "";
1291
1515
  const fontFamily = fontFromCascade || fontFromDeclaration || REQUIRED_TEMPLATE_DEFAULTS.fontFamily;
1292
1516
  const textColor = parseColor(cascade.byTag["mj-text"]?.color) || REQUIRED_TEMPLATE_DEFAULTS.textColor;
@@ -1511,11 +1735,14 @@ function buildSection($el, ctx, entries, wrapper) {
1511
1735
  const slots = COLUMN_COUNT[layout];
1512
1736
  const pixels = columnPixels(layout, ctx.containerWidth);
1513
1737
  const shown = rawWidths.map((w) => w || "auto").join(", ");
1738
+ const notes = exact ? [] : [`Column widths ${shown} have no exact Templatical layout; resolved to "${layout}".`];
1739
+ const borderRadius = readBorderRadius(attrs, "Section", notes);
1740
+ const border = readBorder(attrs, "Section", notes);
1514
1741
  entries.push({
1515
1742
  sourceTag: "mj-section",
1516
1743
  templaticalBlockType: "section",
1517
- status: exact ? "converted" : "approximated",
1518
- ...exact ? {} : { note: `Column widths ${shown} have no exact Templatical layout; resolved to "${layout}".` }
1744
+ status: notes.length > 0 ? "approximated" : "converted",
1745
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
1519
1746
  });
1520
1747
  const children = Array.from({ length: slots }, () => []);
1521
1748
  columns.forEach(($column, index) => {
@@ -1528,24 +1755,40 @@ function buildSection($el, ctx, entries, wrapper) {
1528
1755
  };
1529
1756
  children[slot].push(...convertColumnChildren($column, columnCtx, entries));
1530
1757
  });
1531
- const borderRadius = parsePxValue(attrs["border-radius"]);
1532
1758
  return [createSectionBlock({
1533
1759
  columns: layout,
1534
1760
  children,
1535
1761
  ...grouped ? { stackOnMobile: false } : {},
1536
- ...borderRadius > 0 ? { borderRadius } : {},
1762
+ ...borderRadius ? { borderRadius } : {},
1763
+ ...border ? { border } : {},
1537
1764
  ...wrapper ? { wrapper } : {},
1538
1765
  ...baseFields(attrs, "native")
1539
1766
  })];
1540
1767
  }
1541
- function readWrapper(attrs) {
1768
+ const WRAPPER_BORDER_ATTRIBUTES = [
1769
+ "border",
1770
+ "border-top",
1771
+ "border-right",
1772
+ "border-bottom",
1773
+ "border-left"
1774
+ ];
1775
+ /**
1776
+ * `SectionWrapper` has no border, so each `border` or `border-<side>` the
1777
+ * wrapper sets pushes a note instead.
1778
+ */
1779
+ function readWrapper(attrs, notes) {
1542
1780
  const backgroundColor = parseColor(attrs["background-color"]);
1543
1781
  const padding = parsePaddingShorthand(attrs.padding);
1544
- const borderRadius = parsePxValue(attrs["border-radius"]);
1782
+ const borderRadius = readBorderRadius(attrs, "Wrapper", notes);
1783
+ for (const name of WRAPPER_BORDER_ATTRIBUTES) {
1784
+ const raw = attrs[name];
1785
+ if (raw === void 0) continue;
1786
+ notes.push(`Wrapper ${name} "${raw.trim()}" has no Templatical equivalent; dropped.`);
1787
+ }
1545
1788
  return {
1546
1789
  ...backgroundColor ? { backgroundColor } : {},
1547
1790
  padding,
1548
- ...borderRadius > 0 ? { borderRadius } : {}
1791
+ ...borderRadius ? { borderRadius } : {}
1549
1792
  };
1550
1793
  }
1551
1794
  /**
@@ -1556,7 +1799,9 @@ function readWrapper(attrs) {
1556
1799
  * as its own section would double the nesting on every round trip.
1557
1800
  */
1558
1801
  function buildWrapper($el, ctx, entries) {
1559
- const wrapper = readWrapper(resolveAttributes($el, ctx.cascade));
1802
+ const attrs = resolveAttributes($el, ctx.cascade);
1803
+ const notes = [];
1804
+ const wrapper = readWrapper(attrs, notes);
1560
1805
  const sections = childElements($el, ctx.$).filter(($k) => tagOf($k[0]) === "mj-section");
1561
1806
  if (sections.length === 0) {
1562
1807
  entries.push({
@@ -1567,11 +1812,12 @@ function buildWrapper($el, ctx, entries) {
1567
1812
  });
1568
1813
  return [];
1569
1814
  }
1570
- if (sections.length > 1) entries.push({
1815
+ if (sections.length > 1) notes.unshift(`An <mj-wrapper> holding ${sections.length} sections was applied to each of them — Templatical has no multi-section band.`);
1816
+ if (notes.length > 0) entries.push({
1571
1817
  sourceTag: "mj-wrapper",
1572
1818
  templaticalBlockType: "section",
1573
1819
  status: "approximated",
1574
- note: `An <mj-wrapper> holding ${sections.length} sections was applied to each of them — Templatical has no multi-section band.`
1820
+ note: notes.join(" ")
1575
1821
  });
1576
1822
  return sections.flatMap(($section) => buildSection($section, ctx, entries, wrapper));
1577
1823
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/attribute-resolver.ts","../src/attribute-parser.ts","../src/block-base.ts","../src/composite-mapper.ts","../src/text-inference.ts","../src/block-mapper.ts","../src/display-condition.ts","../src/head-parser.ts","../src/section-builder.ts","../src/converter.ts"],"sourcesContent":["import type { Cheerio, CheerioAPI } from \"cheerio\";\nimport type { AnyNode, Element } from \"domhandler\";\nimport type { BlockVisibility } from \"@templatical/types\";\n\nexport type Attrs = Record<string, string>;\n\n/**\n * The flattened contents of `mj-head > mj-attributes`, built once per document.\n */\nexport interface AttributeCascade {\n /** From `<mj-all …>` — applies to every element. */\n all: Attrs;\n /** From `<mj-text …>` etc. — keyed by lowercased tag name. */\n byTag: Record<string, Attrs>;\n /** From `<mj-class name=\"x\" …>` — keyed by class name. */\n byClass: Record<string, Attrs>;\n}\n\n/**\n * A node's tag name, lowercased.\n *\n * The parser configuration this package uses (`xmlMode: false`, set in\n * `converter.ts`) already lowercases every tag at parse time, so this call is\n * a no-op on that parser's output. It remains the one place every tag\n * comparison in this package goes through, so a comparison stays correct\n * regardless of the active parser configuration — a bare `$(\"mj-body\")` would\n * otherwise miss a document that shouts its tags.\n */\nexport function tagOf(node: AnyNode | undefined): string {\n if (!node) return \"\";\n return (node as Element).tagName?.toLowerCase() ?? \"\";\n}\n\n/**\n * Every element with the given tag name, matched case-insensitively.\n */\nexport function findByTag($: CheerioAPI, tag: string): Cheerio<Element> {\n const wanted = tag.toLowerCase();\n return $(\"*\").filter(\n (_, el) => tagOf(el) === wanted,\n ) as unknown as Cheerio<Element>;\n}\n\n/**\n * An element's element children.\n *\n * `.children()` already excludes text and comment nodes; the tag filter here\n * guards against a node whose `tagName` is undefined, not against either of\n * those.\n */\nexport function childElements(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n): Cheerio<Element>[] {\n return $el\n .children()\n .toArray()\n .filter((node) => tagOf(node) !== \"\")\n .map((node) => $(node) as unknown as Cheerio<Element>);\n}\n\nfunction attrsOf($el: Cheerio<Element>): Attrs {\n const raw = $el.attr();\n if (!raw) return {};\n const out: Attrs = {};\n for (const [key, value] of Object.entries(raw)) {\n out[key.toLowerCase()] = value;\n }\n return out;\n}\n\n/**\n * Read `mj-head > mj-attributes` into the three buckets the cascade resolves\n * against. Called once per document; `resolveAttributes` is then pure lookup.\n */\nexport function buildAttributeCascade($: CheerioAPI): AttributeCascade {\n // Every bucket is a lookup table keyed by names taken from the imported\n // document, so a class or tag literally named `__proto__` must not be able\n // to reach the bucket's own prototype — `Object.create(null)` means a\n // computed-key write like `cascade.byClass[name] = …` can only ever create\n // an own property, never reassign what the bucket inherits from.\n const cascade: AttributeCascade = {\n all: Object.create(null),\n byTag: Object.create(null),\n byClass: Object.create(null),\n };\n\n const containers = findByTag($, \"mj-attributes\").toArray();\n for (const container of containers) {\n const $container = $(container) as unknown as Cheerio<Element>;\n for (const $child of childElements($container, $)) {\n const tag = tagOf($child[0]);\n const attrs = attrsOf($child);\n\n if (tag === \"mj-all\") {\n Object.assign(cascade.all, attrs);\n continue;\n }\n\n if (tag === \"mj-class\") {\n const { name, ...rest } = attrs;\n if (!name) continue;\n cascade.byClass[name] = { ...(cascade.byClass[name] ?? {}), ...rest };\n continue;\n }\n\n cascade.byTag[tag] = { ...(cascade.byTag[tag] ?? {}), ...attrs };\n }\n }\n\n return cascade;\n}\n\n/**\n * The first two cascade layers alone — `mj-all` then the per-tag default —\n * with neither a named `mj-class` nor the element's own inline attributes on\n * top. This is the ambient value a tag inherits before the element sets\n * anything of its own, which is what {@link ownAttr} compares against.\n */\nfunction resolveTagDefaults(tag: string, cascade: AttributeCascade): Attrs {\n return {\n ...cascade.all,\n ...(cascade.byTag[tag] ?? {}),\n };\n}\n\n/**\n * An element's effective attributes, highest precedence last:\n * `mj-all` → per-tag default → each named `mj-class` → the element's own\n * inline attributes.\n *\n * MJML's built-in component defaults are deliberately not modelled — the block\n * factories in `@templatical/types` supply that floor instead, which keeps this\n * module from becoming a copy of MJML's component table.\n */\nexport function resolveAttributes(\n $el: Cheerio<Element>,\n cascade: AttributeCascade,\n): Attrs {\n const own = attrsOf($el);\n const tag = tagOf($el[0]);\n\n const resolved: Attrs = resolveTagDefaults(tag, cascade);\n\n const classNames = (own[\"mj-class\"] ?? \"\")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n for (const name of classNames) {\n Object.assign(resolved, cascade.byClass[name] ?? {});\n }\n\n Object.assign(resolved, own);\n delete resolved[\"mj-class\"];\n\n return resolved;\n}\n\n/**\n * A resolved attribute value, but only when the element is the reason it has\n * that value — a named `mj-class` counts as the element opting in, but a bare\n * `mj-all`/per-tag cascade default does not.\n *\n * This reverses the renderer's inherit-by-omission convention for `mj-text`'s\n * `color` and `font-family`: `renderers/title.ts`, `table.ts` and `menu.ts`\n * all emit that attribute only when the block sets its own value, otherwise\n * leaving the element to inherit the document default from\n * `<mj-attributes>` (`renderers/title.ts:31`). `resolveAttributes` flattens\n * that default onto every element of the tag regardless, so reading `attrs`\n * directly cannot tell \"the block set this\" from \"the document default\n * reached this element too\" — and treating the latter as the former invents\n * a per-block override the source never made.\n *\n * `resolved === ambient` also covers an element that repeats the ambient\n * value on purpose: the two cases render identically, so which one happened\n * is not observable in the output and dropping it costs nothing (the same\n * reasoning as §8.3b's image-width and §8.4b's paragraph-gap defaults).\n */\nexport function ownAttr(\n attrs: Attrs,\n key: string,\n tag: string,\n cascade: AttributeCascade,\n): string | undefined {\n const resolved = attrs[key];\n if (resolved === undefined) return undefined;\n return resolved === resolveTagDefaults(tag, cascade)[key]\n ? undefined\n : resolved;\n}\n\nconst HIDE_DESKTOP = \"tpl-hide-desktop\";\nconst HIDE_MOBILE = \"tpl-hide-mobile\";\n\n/**\n * Marks a rendered title or paragraph's rich-text spacing — mirrors\n * `RICH_TEXT_CSS_CLASS` in `packages/renderer/src/rich-text.ts`. `title.ts`\n * and `paragraph.ts` are the only two renderers that pass a second argument\n * to `getCssClassAttr`, so every rendered title and paragraph carries this\n * class on `css-class` alongside any visibility markers.\n */\nconst RICH_TEXT_CSS_CLASS = \"tpl-rich-text\";\n\n/**\n * Matches the per-block paragraph-gap class the same two renderers append,\n * e.g. `tpl-rich-text-8` (`richTextGapClass` in `rich-text.ts`). The gap\n * accepts a decimal (`tpl-rich-text-8.5`) because `ParagraphBlock.paragraphSpacing`\n * is a plain `number` that a headless caller can set to a fractional value,\n * even though the editor UI clamps it to an integer. Negative gaps are out of\n * scope — the renderer never emits one, so there is nothing here that needs to\n * recognise it.\n *\n * Shared by `readForeignCssClasses` (which classes to exclude) and\n * `readParagraphGap` (which class to decode), so the two can never disagree\n * about what a gap class looks like.\n */\nconst RICH_TEXT_GAP_CLASS = /^tpl-rich-text-(\\d+(?:\\.\\d+)?)$/;\n\nfunction cssClasses(attrs: Attrs): string[] {\n return (attrs[\"css-class\"] ?? \"\").trim().split(/\\s+/).filter(Boolean);\n}\n\n/**\n * Reverse of the renderer's `getCssClassAttr` (`packages/renderer/src/visibility.ts`).\n * `css-class` also carries the rich-text markers below (`RICH_TEXT_CSS_CLASS`,\n * `RICH_TEXT_GAP_CLASS`) — this function reads only the two visibility\n * classes off it and leaves those alone.\n *\n * Returns `undefined` when neither class is present — absence means visible\n * everywhere, and writing `{ desktop: true, mobile: true }` instead would put a\n * redundant key in every imported block.\n */\nexport function readVisibility(attrs: Attrs): BlockVisibility | undefined {\n const classes = cssClasses(attrs);\n const hideDesktop = classes.includes(HIDE_DESKTOP);\n const hideMobile = classes.includes(HIDE_MOBILE);\n\n if (!hideDesktop && !hideMobile) return undefined;\n\n return { desktop: !hideDesktop, mobile: !hideMobile };\n}\n\n/**\n * The paragraph gap encoded on a rendered title or paragraph's `css-class` —\n * reverse of `richTextGapClass` (`packages/renderer/src/rich-text.ts`), e.g.\n * `8` from `tpl-rich-text-8`.\n *\n * Returns `null` when no gap class is present. Callers must not confuse that\n * with a gap of `0`, which is a legitimate value in its own right.\n */\nexport function readParagraphGap(attrs: Attrs): number | null {\n for (const name of cssClasses(attrs)) {\n const match = RICH_TEXT_GAP_CLASS.exec(name);\n if (match) return parseFloat(match[1]);\n }\n return null;\n}\n\n/**\n * Classes on `css-class` that carry no Templatical meaning. The caller warns\n * about these rather than dropping them silently: they are consumer CSS with no\n * home in the block model, and a template that relies on them will render\n * differently after import.\n *\n * Excludes the renderer's own rich-text markers alongside the two visibility\n * classes, so importing a template the renderer itself produced does not\n * report a title or paragraph's own spacing class as foreign. A consumer's\n * own `tpl-`-prefixed class is not one of these markers and is still reported.\n */\nexport function readForeignCssClasses(attrs: Attrs): string[] {\n return cssClasses(attrs).filter(\n (name) =>\n name !== HIDE_DESKTOP &&\n name !== HIDE_MOBILE &&\n name !== RICH_TEXT_CSS_CLASS &&\n !RICH_TEXT_GAP_CLASS.test(name),\n );\n}\n","import type { SpacingValue } from \"@templatical/types\";\n\n/**\n * Parses a px-like MJML attribute value (`\"12px\"`, `\"12\"`, `12`) into a rounded\n * integer. Returns 0 for missing or unparseable input, and for units the block\n * model cannot express (em, rem, %) — a caller that needs to tell \"absent\" from\n * \"0\" must check the raw attribute itself.\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 match = value.match(/^\\s*(-?\\d+(?:\\.\\d+)?)\\s*(?:px)?\\s*$/);\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 * Normalizes a colour value to a 6-digit lowercase hex string.\n *\n * Returns `\"\"` for transparent/inherit/none and for anything unrecognised.\n * The empty string is the block model's \"unset\" — the colour pickers clear to\n * it — so returning 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\n if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed;\n\n if (/^#[0-9a-f]{3}$/.test(trimmed)) {\n const r = trimmed[1];\n const g = trimmed[2];\n const b = trimmed[3];\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n\n const rgbMatch = trimmed.match(\n /^rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(?:,\\s*[\\d.]+\\s*)?\\)$/,\n );\n if (rgbMatch) {\n return rgbToHex(\n parseInt(rgbMatch[1], 10),\n parseInt(rgbMatch[2], 10),\n parseInt(rgbMatch[3], 10),\n );\n }\n\n if (NAMED_COLORS[trimmed]) return NAMED_COLORS[trimmed];\n\n return \"\";\n}\n\n/**\n * Parses an MJML `padding` shorthand (1-4 values, CSS order) into a\n * SpacingValue.\n */\nexport function parsePaddingShorthand(value: string | undefined): SpacingValue {\n if (!value) return { top: 0, right: 0, bottom: 0, left: 0 };\n\n const values = value\n .trim()\n .split(/\\s+/)\n .map((p) => parsePxValue(p));\n\n switch (values.length) {\n case 1:\n return {\n top: values[0],\n right: values[0],\n bottom: values[0],\n left: values[0],\n };\n case 2:\n return {\n top: values[0],\n right: values[1],\n bottom: values[0],\n left: values[1],\n };\n case 3:\n return {\n top: values[0],\n right: values[1],\n bottom: values[2],\n left: values[1],\n };\n default:\n return {\n top: values[0],\n right: values[1],\n bottom: values[2],\n left: values[3],\n };\n }\n}\n\n/**\n * Strips quotes and returns the first font in a font-family stack.\n */\nexport function parseFontFamily(value: string | undefined): string {\n if (!value) return \"\";\n return value\n .split(\",\")[0]\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\");\n}\n\n/**\n * Parses an alignment to one of the three the block model accepts.\n */\nexport function parseAlignment(\n value: string | undefined,\n fallback: \"left\" | \"center\" | \"right\" = \"left\",\n): \"left\" | \"center\" | \"right\" {\n const v = (value ?? \"\").trim().toLowerCase();\n if (v === \"left\" || v === \"center\" || v === \"right\") return v;\n return fallback;\n}\n\n/**\n * Reads a percentage value, or `null` when the value is not a percentage.\n *\n * `null` rather than a number, because column-width matching (§8.1) has to tell\n * \"no percentage given\" from \"0%\" — the former distributes widths equally, the\n * latter is a real (if degenerate) width.\n */\nexport function parsePercent(value: string | undefined): number | null {\n if (!value) return null;\n const match = value.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*%$/);\n return match ? parseFloat(match[1]) : null;\n}\n\n/**\n * Reads a definite px length, or `null` when the value is not one.\n *\n * Unlike `parsePxValue`, which returns `0` for anything unparseable, this\n * tells \"no length given\" apart from \"0px\" — column-width recovery (§8.1)\n * needs that distinction for a px `mj-column` width the same way it needs\n * `parsePercent`'s `null` for a percentage one, so the two compose into a\n * single known-or-absent value the matcher can fill around.\n */\nexport function parseDefinitePx(value: string | undefined): number | null {\n if (!value) return null;\n const match = value.trim().match(/^(-?\\d+(?:\\.\\d+)?)\\s*(?:px)?$/);\n return match ? parseFloat(match[1]) : null;\n}\n\n/**\n * Narrows a border style to the three `DividerBlock.lineStyle` accepts.\n */\nexport function parseBorderStyle(\n value: string | undefined,\n): \"solid\" | \"dashed\" | \"dotted\" {\n const v = (value ?? \"\").trim().toLowerCase();\n if (v === \"dashed\" || v === \"dotted\") return v;\n return \"solid\";\n}\n","import type { Cheerio, CheerioAPI } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport { createHtmlBlock } from \"@templatical/types\";\nimport type {\n Block,\n BlockStyles,\n BlockVisibility,\n ContentDirection,\n} from \"@templatical/types\";\nimport { parseColor, parsePaddingShorthand } from \"./attribute-parser\";\nimport {\n readForeignCssClasses,\n readVisibility,\n type Attrs,\n type AttributeCascade,\n} from \"./attribute-resolver\";\nimport type { ImportReportEntry } from \"./types\";\n\n/**\n * Everything a converter needs beyond the element itself.\n *\n * `containerWidth` is the width the element renders at — a section's column\n * width, or `settings.width` at top level. It is what lets an `mj-image` whose\n * px width equals its container restore `width: \"full\"` (§8.3b), and what an\n * `mj-divider`'s px width is measured against, less `columnPadding` and the\n * divider's own side padding.\n *\n * `columnPadding` is the side padding (left plus right, px) of the\n * `mj-column` holding the element, 0 at top level. A Templatical column has no\n * padding of its own, so this is read only where it changes what a value\n * means: the width an `mj-divider` line can span.\n *\n * `direction` is the writing direction the imported template resolves to. Its\n * start edge is where a paragraph sits when its markup states no alignment,\n * so an `mj-text` aligned to that edge needs none.\n */\nexport interface ConvertContext {\n $: CheerioAPI;\n cascade: AttributeCascade;\n containerWidth: number;\n columnPadding: number;\n direction: ContentDirection;\n warnings: string[];\n}\n\n/** A produced block plus its report entry. `block` is null only when skipped. */\nexport interface Converted {\n block: Block | null;\n entry: ImportReportEntry;\n}\n\n/**\n * The `styles` and `visibility` every block shares.\n *\n * `placement` mirrors the renderer's own `BgPlacement` split\n * (`packages/renderer/src/utils.ts`): `mj-section` is the one \"native\"\n * element, whose own `background-color` attribute *is* `styles.backgroundColor`\n * (`renderers/section.ts:32`). Every other block type emits\n * `container-background-color` for that same field, because a plain\n * `background-color` on those tags already means something else of the\n * element's own — a button's fill (`renderers/button.ts:43`), for one — and\n * reading it back as the block's container background would invent a fill\n * the source never had. Defaulting to `\"container\"` matches every caller but\n * `buildSection`.\n *\n * `visibility` is spread conditionally so an unset block carries no key — the\n * block model treats absence as \"visible everywhere\".\n */\nexport function baseFields(\n attrs: Attrs,\n placement: \"container\" | \"native\" = \"container\",\n): {\n styles: BlockStyles;\n visibility?: BlockVisibility;\n} {\n const bgKey =\n placement === \"native\" ? \"background-color\" : \"container-background-color\";\n const backgroundColor = parseColor(attrs[bgKey]);\n const visibility = readVisibility(attrs);\n\n return {\n styles: {\n padding: parsePaddingShorthand(attrs.padding),\n ...(backgroundColor ? { backgroundColor } : {}),\n },\n ...(visibility ? { visibility } : {}),\n };\n}\n\nexport function warnForeignClasses(\n attrs: Attrs,\n tag: string,\n ctx: ConvertContext,\n): void {\n for (const name of readForeignCssClasses(attrs)) {\n ctx.warnings.push(\n `Dropped CSS class \"${name}\" on <${tag}> — consumer CSS has no Templatical equivalent.`,\n );\n }\n}\n\nexport function isNewTab(attrs: Attrs): boolean {\n return (attrs.target ?? \"\").trim().toLowerCase() === \"_blank\";\n}\n\n/**\n * Wrap the element's own markup in an HTML block — the lossless fallback.\n */\nexport function convertHtmlFallback(\n $el: Cheerio<Element>,\n ctx: ConvertContext,\n attrs: Attrs,\n): Block {\n const outer = ctx.$.html($el) ?? \"\";\n return createHtmlBlock({ content: outer, ...baseFields(attrs) });\n}\n","import type { Cheerio } from \"cheerio\";\nimport {\n createMenuBlock,\n createSocialIconsBlock,\n createTableBlock,\n generateId,\n} from \"@templatical/types\";\nimport type {\n MenuItemData,\n SocialIcon,\n SocialIconSize,\n SocialIconStyle,\n SocialPlatform,\n TableRowData,\n} from \"@templatical/types\";\nimport type { Element } from \"domhandler\";\nimport {\n parseAlignment,\n parseColor,\n parseFontFamily,\n parsePxValue,\n} from \"./attribute-parser\";\nimport {\n childElements,\n ownAttr,\n resolveAttributes,\n tagOf,\n type Attrs,\n} from \"./attribute-resolver\";\nimport { baseFields, type ConvertContext, type Converted } from \"./block-base\";\n\n/**\n * Exhaustive over `SocialPlatform` on purpose: adding a member to that union\n * without adding it here is a compile error, so the importer cannot silently\n * fall back 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 PLATFORM_ALIASES: Record<string, SocialPlatform> = {\n x: \"twitter\",\n \"x-twitter\": \"twitter\",\n};\n\n/**\n * Icon packs (and MJML's `-noshare` variants) append tokens to the platform\n * slug: `facebook-round-outlined.png`, `facebook-noshare`. Those stems are not\n * `SocialPlatform` values, so a lookup that stops at the raw filename maps\n * the icon to `\"website\"` and the platform name is lost.\n */\nconst PLATFORM_PACK_TOKENS = new Set([\n \"noshare\",\n \"round\",\n \"outlined\",\n \"square\",\n \"circle\",\n \"solid\",\n]);\n\nfunction lookupPlatform(cleaned: string): SocialPlatform | null {\n if (PLATFORM_ALIASES[cleaned]) return PLATFORM_ALIASES[cleaned];\n return cleaned in KNOWN_PLATFORMS ? (cleaned as SocialPlatform) : null;\n}\n\nfunction normalizePlatform(raw: string): SocialPlatform | null {\n const cleaned = raw.trim().toLowerCase();\n if (!cleaned) return null;\n\n const direct = lookupPlatform(cleaned);\n if (direct) return direct;\n\n const parts = cleaned.split(\"-\");\n while (\n parts.length > 1 &&\n PLATFORM_PACK_TOKENS.has(parts[parts.length - 1])\n ) {\n parts.pop();\n const candidate = parts.join(\"-\");\n const hit = lookupPlatform(candidate);\n if (hit) return hit;\n }\n return null;\n}\n\n/** The `<style>/<platform>.png` tail of the URL `renderers/social.ts:76` builds. */\nfunction platformFromSrc(src: string): { platform: string; style: string } {\n const parts = src.split(\"?\")[0].split(\"/\").filter(Boolean);\n const file = parts.at(-1) ?? \"\";\n return {\n platform: file.replace(/\\.[a-z0-9]+$/i, \"\"),\n style: parts.at(-2) ?? \"\",\n };\n}\n\nconst ICON_SIZES: Array<[number, SocialIconSize]> = [\n [24, \"small\"],\n [32, \"medium\"],\n [48, \"large\"],\n];\n\nfunction nearestIconSize(px: number): { size: SocialIconSize; exact: boolean } {\n let best = ICON_SIZES[1];\n let bestGap = Infinity;\n for (const candidate of ICON_SIZES) {\n const gap = Math.abs(candidate[0] - px);\n if (gap < bestGap) {\n bestGap = gap;\n best = candidate;\n }\n }\n return { size: best[1], exact: bestGap === 0 };\n}\n\nconst RADIUS_STYLES: Record<string, SocialIconStyle> = {\n \"50%\": \"circle\",\n \"8px\": \"rounded\",\n \"0\": \"square\",\n \"4px\": \"solid\",\n};\n\nconst KNOWN_ICON_STYLES = new Set<string>([\n \"solid\",\n \"outlined\",\n \"rounded\",\n \"square\",\n \"circle\",\n]);\n\nexport function convertSocial(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted | null {\n const elements = childElements($el, ctx.$).filter(\n ($child) => tagOf($child[0]) === \"mj-social-element\",\n );\n if (elements.length === 0) return null;\n\n const notes: string[] = [];\n const icons: SocialIcon[] = [];\n\n let iconStyle: SocialIconStyle | null = null;\n let iconSizePx = 0;\n let spacing = 0;\n\n elements.forEach(($child, index) => {\n const childAttrs = resolveAttributes($child, ctx.cascade);\n const src = (childAttrs.src ?? \"\").trim();\n const fromSrc = src ? platformFromSrc(src) : { platform: \"\", style: \"\" };\n\n const name = (childAttrs.name ?? \"\").trim();\n const alt = (childAttrs.alt ?? \"\").trim();\n const platform =\n normalizePlatform(name) ??\n normalizePlatform(fromSrc.platform) ??\n normalizePlatform(alt);\n if (!platform) {\n const rawName = name || fromSrc.platform || alt;\n if (rawName) {\n notes.push(\n `Unrecognised social platform \"${rawName}\" mapped to \"website\".`,\n );\n }\n }\n\n icons.push({\n id: generateId(),\n platform: platform ?? \"website\",\n url: (childAttrs.href ?? \"\").trim(),\n });\n\n if (!iconStyle && KNOWN_ICON_STYLES.has(fromSrc.style)) {\n iconStyle = fromSrc.style as SocialIconStyle;\n }\n\n if (iconSizePx === 0) {\n iconSizePx = parsePxValue(childAttrs[\"icon-size\"]);\n }\n\n // The final element emits `0` right-padding, so spacing is only readable\n // from a non-final one (renderers/social.ts:79).\n if (spacing === 0 && index < elements.length - 1) {\n spacing = parsePxValue((childAttrs.padding ?? \"\").trim().split(/\\s+/)[1]);\n }\n\n if (!iconStyle) {\n const radius = (childAttrs[\"border-radius\"] ?? \"\").trim().toLowerCase();\n const mapped = RADIUS_STYLES[radius];\n if (mapped) {\n iconStyle = mapped;\n if (radius === \"4px\") {\n notes.push(\n 'Icon border-radius 4px maps to both \"solid\" and \"outlined\"; resolved to \"solid\".',\n );\n }\n }\n }\n });\n\n const declaredSize = parsePxValue(attrs[\"icon-size\"]) || iconSizePx;\n let iconSize: SocialIconSize | undefined;\n if (declaredSize > 0) {\n const resolved = nearestIconSize(declaredSize);\n iconSize = resolved.size;\n if (!resolved.exact) {\n notes.push(\n `Icon size ${declaredSize}px is not one of 24/32/48; resolved to \"${resolved.size}\".`,\n );\n }\n }\n\n const block = createSocialIconsBlock({\n icons,\n align: parseAlignment(attrs.align, \"center\"),\n ...(iconSize ? { iconSize } : {}),\n ...(iconStyle ? { iconStyle } : {}),\n ...(spacing > 0 ? { spacing } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\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\nexport function convertNavbar(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted | null {\n const links = childElements($el, ctx.$).filter(\n ($child) => tagOf($child[0]) === \"mj-navbar-link\",\n );\n if (links.length === 0) return null;\n\n const items: MenuItemData[] = links.map(($link) => {\n const linkAttrs = resolveAttributes($link, ctx.cascade);\n // ownAttr, not linkAttrs directly — same cascade-vs-own distinction as\n // the block-level font-family read below: a hand-written document's own\n // <mj-attributes><mj-navbar-link color=\"…\"/></mj-attributes> default must\n // not be read back as this link's own override.\n const color = parseColor(\n ownAttr(linkAttrs, \"color\", \"mj-navbar-link\", ctx.cascade),\n );\n return {\n id: generateId(),\n text: ($link.text() ?? \"\").trim(),\n url: (linkAttrs.href ?? \"\").trim(),\n openInNewTab: (linkAttrs.target ?? \"\").toLowerCase() === \"_blank\",\n bold: (linkAttrs[\"font-weight\"] ?? \"\").toLowerCase() === \"bold\",\n underline: (linkAttrs[\"text-decoration\"] ?? \"\").includes(\"underline\"),\n ...(color ? { color } : {}),\n };\n });\n\n const fontSize = parsePxValue(attrs[\"font-size\"]);\n // ownAttr, not attrs directly — same cascade-vs-own distinction ownAttr\n // documents (attribute-resolver.ts): our own renderer never emits\n // mj-navbar, but a hand-written document's own\n // <mj-attributes><mj-navbar font-family=\"…\"/></mj-attributes> default must\n // not be read back as this navbar's own override.\n const fontFamily = parseFontFamily(\n ownAttr(attrs, \"font-family\", \"mj-navbar\", ctx.cascade),\n );\n\n const block = createMenuBlock({\n items,\n textAlign: parseAlignment(attrs.align, \"center\"),\n ...(fontSize > 0 ? { fontSize } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-navbar\",\n templaticalBlockType: \"menu\",\n status: \"converted\",\n },\n };\n}\n\nexport function convertNativeTable(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted | null {\n const $ = ctx.$;\n // .find(\"tr\") is a descendant search, so a <table> nested inside a cell\n // would otherwise contribute its own rows here too, interleaved with\n // $el's own. Keep only rows with no \"table\" ancestor between themselves\n // and $el — a nested table's rows stay untouched inside their cell's\n // content, read verbatim by the .html() call below.\n const rowEls = $el\n .find(\"tr\")\n .toArray()\n .filter((tr) => $(tr).parentsUntil($el, \"table\").length === 0);\n if (rowEls.length === 0) return null;\n\n const rows: TableRowData[] = rowEls.map((rowEl) => ({\n id: generateId(),\n cells: $(rowEl)\n .children()\n .toArray()\n .map((cellEl) => ({ id: generateId(), content: $(cellEl).html() ?? \"\" })),\n }));\n\n const hasHeaderRow = $(rowEls[0])\n .children()\n .toArray()\n .some((cell) => tagOf(cell) === \"th\");\n\n // ownAttr, not attrs directly — same cascade-vs-own distinction as\n // convertNavbar above: a hand-written document's own\n // <mj-attributes><mj-table color=\"…\" font-family=\"…\"/></mj-attributes>\n // default must not be read back as this table's own override.\n const color = parseColor(ownAttr(attrs, \"color\", \"mj-table\", ctx.cascade));\n const fontSize = parsePxValue(attrs[\"font-size\"]);\n const fontFamily = parseFontFamily(\n ownAttr(attrs, \"font-family\", \"mj-table\", ctx.cascade),\n );\n\n const block = createTableBlock({\n rows,\n hasHeaderRow,\n textAlign: parseAlignment(attrs.align, \"left\"),\n ...(color ? { color } : {}),\n ...(fontSize > 0 ? { fontSize } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-table\",\n templaticalBlockType: \"table\",\n status: \"converted\",\n },\n };\n}\n","import { load } from \"cheerio\";\nimport type { Cheerio, CheerioAPI } from \"cheerio\";\nimport { isTag, isText } from \"domhandler\";\nimport type { AnyNode, Element } from \"domhandler\";\nimport {\n createMenuBlock,\n createParagraphBlock,\n createTableBlock,\n createTitleBlock,\n generateId,\n RICH_TEXT_SPACING,\n} from \"@templatical/types\";\nimport type {\n HeadingLevel,\n MenuItemData,\n TableRowData,\n} from \"@templatical/types\";\nimport {\n parseAlignment,\n parseColor,\n parseFontFamily,\n parsePxValue,\n} from \"./attribute-parser\";\nimport {\n ownAttr,\n readParagraphGap,\n type Attrs,\n type AttributeCascade,\n} from \"./attribute-resolver\";\nimport { baseFields, type ConvertContext, type Converted } from \"./block-base\";\n\n/**\n * Re-parse an `mj-text`'s inner markup into its own document.\n *\n * The surrounding document already parses void elements correctly (`br`,\n * `img`, … stay void under `converter.ts`'s `xmlMode: false`), so this reparse\n * isn't compensating for a different parser here. It gives each\n * shape-detection helper (title / table / menu) an isolated, freshly\n * queryable document scoped to just this block's own markup — so\n * `$inner(\"tr\")` can only match rows that belong to this table, and\n * `$inner(\"body\")` has a real root to enumerate top-level nodes from. A\n * paragraph's markup is reparsed only when an attribute has to be carried\n * into it (`styleParagraphs`); otherwise it passes through verbatim.\n */\nfunction parseInner(html: string) {\n return load(`<body>${html}</body>`);\n}\n\nfunction rootElements(html: string): { tag: string; count: number } {\n const $inner = parseInner(html);\n const kids = $inner(\"body\").children().toArray();\n return {\n tag: kids.length > 0 ? (kids[0].tagName?.toLowerCase() ?? \"\") : \"\",\n count: kids.length,\n };\n}\n\nconst HEADING_LEVELS: Record<string, number> = {\n h1: 1,\n h2: 2,\n h3: 3,\n h4: 4,\n h5: 5,\n h6: 6,\n};\n\nfunction convertTitle(\n html: string,\n attrs: Attrs,\n sourceLevel: number,\n cascade: AttributeCascade,\n): Converted {\n const $inner = parseInner(html);\n const $heading = $inner(\"body\").children().first();\n const level = Math.min(sourceLevel, 4) as HeadingLevel;\n // Read via ownAttr, not attrs directly: title.ts emits color/font-family\n // only when the block sets its own, so a value that only reached this\n // element through the document-wide `<mj-attributes>` cascade (§7) is not\n // this title's own and must not be read back onto it.\n const color = parseColor(ownAttr(attrs, \"color\", \"mj-text\", cascade));\n const fontFamily = parseFontFamily(\n ownAttr(attrs, \"font-family\", \"mj-text\", cascade),\n );\n\n const block = createTitleBlock({\n content: $heading.html() ?? \"\",\n level,\n textAlign: parseAlignment(attrs.align, \"left\"),\n ...(color ? { color } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseFields(attrs),\n });\n\n const clamped = sourceLevel > 4;\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"title\",\n status: clamped ? \"approximated\" : \"converted\",\n ...(clamped\n ? {\n note: `Heading level h${sourceLevel} clamped to 4 — Templatical titles support h1-h4.`,\n }\n : {}),\n },\n };\n}\n\nfunction convertTable(\n html: string,\n attrs: Attrs,\n cascade: AttributeCascade,\n): Converted {\n const $inner = parseInner(html);\n const $rows = $inner(\"tr\");\n\n const rows: TableRowData[] = $rows.toArray().map((rowEl) => ({\n id: generateId(),\n cells: $inner(rowEl)\n .children()\n .toArray()\n .map((cellEl) => ({\n id: generateId(),\n content: $inner(cellEl).html() ?? \"\",\n })),\n }));\n\n const hasHeaderRow =\n $rows.length > 0 && $inner($rows[0]).children(\"th\").length > 0;\n\n // ownAttr, not attrs directly — same cascade-vs-own distinction as\n // convertTitle above: table.ts also emits color/font-family only when the\n // block sets its own (renderers/table.ts:34,29).\n const color = parseColor(ownAttr(attrs, \"color\", \"mj-text\", cascade));\n const fontSize = parsePxValue(attrs[\"font-size\"]);\n const fontFamily = parseFontFamily(\n ownAttr(attrs, \"font-family\", \"mj-text\", cascade),\n );\n\n const block = createTableBlock({\n rows,\n hasHeaderRow,\n textAlign: parseAlignment(attrs.align, \"left\"),\n ...(color ? { color } : {}),\n ...(fontSize > 0 ? { fontSize } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"table\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * A menu is top-level anchors with optional `<span>` separators between them —\n * exactly what `renderers/menu.ts` emits, and deliberately not matched when a\n * `<p>` wrapper is present (that is a paragraph containing links).\n *\n * `.children()` is elements only. A non-whitespace text-node sibling must\n * veto: an `mj-text` of prose plus a trailing `<a>` becomes a menu labelled\n * from the links and the prose is discarded. Whitespace-only nodes (newlines\n * between `<a>`/`<span>`) must not.\n */\nfunction looksLikeMenu(html: string): boolean {\n const $inner = parseInner(html);\n const kids = $inner(\"body\").contents().toArray();\n if (kids.length === 0) return false;\n\n let anchors = 0;\n for (const kid of kids) {\n if (isText(kid)) {\n if (kid.data.trim() !== \"\") return false;\n continue;\n }\n if (!isTag(kid)) continue;\n const tag = kid.tagName.toLowerCase();\n if (tag === \"a\") anchors += 1;\n else if (tag !== \"span\") return false;\n }\n\n return anchors > 0;\n}\n\nfunction convertMenu(\n html: string,\n attrs: Attrs,\n cascade: AttributeCascade,\n): Converted {\n const $inner = parseInner(html);\n\n const items: MenuItemData[] = $inner(\"body\")\n .children(\"a\")\n .toArray()\n .map((el) => {\n const $a = $inner(el);\n const itemColor = parseColor(\n $a.attr(\"style\")?.match(/color\\s*:\\s*([^;]+)/i)?.[1],\n );\n return {\n id: generateId(),\n text: ($a.text() ?? \"\").trim(),\n url: $a.attr(\"href\") ?? \"\",\n openInNewTab: ($a.attr(\"target\") ?? \"\").toLowerCase() === \"_blank\",\n bold: $a.find(\"strong, b\").length > 0,\n underline: ($a.attr(\"style\") ?? \"\").includes(\"underline\"),\n ...(itemColor ? { color: itemColor } : {}),\n };\n });\n\n const $separator = $inner(\"body\").children(\"span\").first();\n const separator = ($separator.text() ?? \"\").trim();\n const separatorColor = parseColor(\n $separator.attr(\"style\")?.match(/color\\s*:\\s*([^;]+)/i)?.[1],\n );\n const spacing = parsePxValue(\n $separator.attr(\"style\")?.match(/padding\\s*:\\s*0\\s+([\\d.]+px)/i)?.[1],\n );\n\n // ownAttr, not attrs directly — same cascade-vs-own distinction as\n // convertTitle above: menu.ts also emits color/font-family only when the\n // block sets its own (renderers/menu.ts:29,24).\n const color = parseColor(ownAttr(attrs, \"color\", \"mj-text\", cascade));\n const fontSize = parsePxValue(attrs[\"font-size\"]);\n const fontFamily = parseFontFamily(\n ownAttr(attrs, \"font-family\", \"mj-text\", cascade),\n );\n\n const block = createMenuBlock({\n items,\n textAlign: parseAlignment(attrs.align, \"center\"),\n ...(separator ? { separator } : {}),\n ...(separatorColor ? { separatorColor } : {}),\n ...(spacing > 0 ? { spacing } : {}),\n ...(color ? { color } : {}),\n ...(fontSize > 0 ? { fontSize } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"menu\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * The editor's rich-text blocks assume a block-level wrapper, so bare text gets\n * one. Mirrors `ensureParagraphWrapped` in `@templatical/import-html`.\n */\nfunction ensureParagraphWrapped(html: string): string {\n const trimmed = html.trim();\n if (trimmed === \"\") return \"<p></p>\";\n if (/^<(p|h[1-6]|ul|ol|blockquote|div)\\b/i.test(trimmed)) return trimmed;\n return `<p>${trimmed}</p>`;\n}\n\n/**\n * The size the renderer gives every `mj-text` (`<mj-text font-size=\"14px\">` in\n * `packages/renderer/src/index.ts`), so a paragraph at this size needs no span\n * to carry it.\n */\nconst RENDERER_TEXT_SIZE = 14;\n\nconst PARAGRAPH_ALIGNMENTS = new Set([\"left\", \"center\", \"right\", \"justify\"]);\n\n/** `<li>` children that end a run of the item's text instead of joining it. */\nconst LIST_ITEM_BLOCK_TAGS = new Set([\n \"p\",\n \"div\",\n \"ul\",\n \"ol\",\n \"blockquote\",\n \"table\",\n \"pre\",\n \"hr\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n]);\n\n/**\n * What an `mj-text` states about its paragraphs' alignment, colour and size.\n * `ParagraphBlock` has a field for none of them, so they travel in the markup.\n * An empty string states nothing.\n */\ninterface ParagraphStyle {\n textAlign: string;\n color: string;\n fontSize: string;\n}\n\nfunction readParagraphStyle(attrs: Attrs, ctx: ConvertContext): ParagraphStyle {\n // The start edge is where a paragraph sits with no alignment of its own, and\n // the renderer states it on every RTL paragraph (`renderers/paragraph.ts`).\n const align = (attrs.align ?? \"\").trim().toLowerCase();\n const startEdge = ctx.direction === \"rtl\" ? \"right\" : \"left\";\n\n // The resolved size, not ownAttr: a size set document-wide has no setting to\n // land in, so dropping it as ambient would lose it.\n const size = parsePxValue(attrs[\"font-size\"]);\n\n return {\n textAlign:\n PARAGRAPH_ALIGNMENTS.has(align) && align !== startEdge ? align : \"\",\n // A colour set document-wide is `settings.textColor` already.\n color: parseColor(ownAttr(attrs, \"color\", \"mj-text\", ctx.cascade)),\n fontSize: size > 0 && size !== RENDERER_TEXT_SIZE ? `${size}px` : \"\",\n };\n}\n\n/**\n * Put each run of an `<li>`'s inline content in a `<p>`, so that the\n * alignment has a paragraph to sit on.\n */\nfunction wrapListItemText($inner: CheerioAPI, li: Element): void {\n let run: AnyNode[] = [];\n const flush = () => {\n // wrapAll, not before() and append(): before() skips a text node, which\n // is what a run usually starts with, and the text would be lost.\n if (run.some((node) => isTag(node) || (isText(node) && node.data.trim()))) {\n $inner(run).wrapAll(\"<p></p>\");\n }\n run = [];\n };\n\n for (const child of [...li.children]) {\n if (isTag(child) && LIST_ITEM_BLOCK_TAGS.has(child.tagName.toLowerCase())) {\n flush();\n } else {\n run.push(child);\n }\n }\n flush();\n}\n\n/**\n * Align one `<p>` and wrap its content in a span carrying colour and size.\n * The `<p>`'s own `text-align` stays, and its own colour and size move into\n * the span, where they win over the attributes as they did on the `<p>`.\n */\nfunction styleParagraph(\n $inner: CheerioAPI,\n $p: Cheerio<Element>,\n style: ParagraphStyle,\n): void {\n if (style.textAlign && !$p.css(\"text-align\")) {\n $p.css(\"text-align\", style.textAlign);\n }\n\n const color = $p.css(\"color\") || style.color;\n const fontSize = $p.css(\"font-size\") || style.fontSize;\n if ((color || fontSize) && $p.contents().length > 0) {\n $p.css({ color: \"\", \"font-size\": \"\" });\n const $span = $inner(\"<span></span>\").css({\n ...(color ? { color } : {}),\n ...(fontSize ? { \"font-size\": fontSize } : {}),\n });\n $p.wrapInner($span);\n }\n\n if ($p.attr(\"style\") === \"\") $p.removeAttr(\"style\");\n}\n\n/**\n * Carry an `mj-text`'s alignment, colour and size into its paragraphs'\n * markup: `text-align` on each `<p>`, colour and size on one span inside it.\n * That is the shape the paragraph editor keeps through an edit\n * (`packages/editor/src/components/blocks/ParagraphEditor.vue` aligns\n * paragraphs and styles spans); a wrapper element around the paragraphs would\n * be dropped on the first edit.\n *\n * Markup with nothing to apply is returned as it came, unparsed.\n */\nfunction styleParagraphs(html: string, style: ParagraphStyle): string {\n if (!style.textAlign && !style.color && !style.fontSize) return html;\n\n const $inner = parseInner(html);\n $inner(\"li\").each((_, li) => wrapListItemText($inner, li));\n $inner(\"p\").each((_, p) => styleParagraph($inner, $inner(p), style));\n return $inner(\"body\").html() ?? html;\n}\n\nfunction convertParagraph(\n html: string,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted {\n // A custom gap round-trips through `css-class` as `tpl-rich-text-<gap>`\n // (`richTextGapClass` in `packages/renderer/src/rich-text.ts`); the default\n // gap round-trips the same way, so it must be excluded here rather than\n // just relying on absence — otherwise every imported paragraph would carry\n // an explicit (if harmless) `paragraphSpacing` equal to the default.\n const gap = readParagraphGap(attrs);\n const paragraphSpacing =\n gap !== null && gap !== RICH_TEXT_SPACING.paragraphGap ? gap : undefined;\n\n const block = createParagraphBlock({\n content: styleParagraphs(\n ensureParagraphWrapped(html),\n readParagraphStyle(attrs, ctx),\n ),\n ...(paragraphSpacing !== undefined ? { paragraphSpacing } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * Resolve an `mj-text` to Title, Table, Menu or Paragraph by the shape of its\n * content — the reverse of the four renderers that all emit `mj-text`.\n *\n * A fifth renderer emits it too: `HtmlBlock` (`renderers/html.ts`), whose\n * content is arbitrary, so it has no shape to match and lands on the Paragraph\n * fallback. That is irreducible — nothing in the output marks a block's type —\n * and narrowing the Title/Table shapes to compensate would break the common\n * case to serve the rare one. See §10 of the design.\n *\n * Paragraph is the terminal arm and always reachable, so this is total.\n */\nexport function convertTextElement(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted {\n const html = $el.html() ?? \"\";\n const root = rootElements(html);\n\n if (root.count === 1 && HEADING_LEVELS[root.tag]) {\n return convertTitle(html, attrs, HEADING_LEVELS[root.tag], ctx.cascade);\n }\n\n if (root.count === 1 && root.tag === \"table\") {\n return convertTable(html, attrs, ctx.cascade);\n }\n\n if (looksLikeMenu(html)) {\n return convertMenu(html, attrs, ctx.cascade);\n }\n\n return convertParagraph(html, attrs, ctx);\n}\n","import type { Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport {\n createButtonBlock,\n createDividerBlock,\n createHtmlBlock,\n createImageBlock,\n createSpacerBlock,\n} from \"@templatical/types\";\nimport type { Block, DividerBlock } from \"@templatical/types\";\nimport {\n parseAlignment,\n parseBorderStyle,\n parseColor,\n parsePaddingShorthand,\n parsePxValue,\n} from \"./attribute-parser\";\nimport { resolveAttributes, tagOf, type Attrs } from \"./attribute-resolver\";\nimport {\n baseFields,\n convertHtmlFallback,\n isNewTab,\n warnForeignClasses,\n type ConvertContext,\n type Converted,\n} from \"./block-base\";\nimport {\n convertNativeTable,\n convertNavbar,\n convertSocial,\n} from \"./composite-mapper\";\nimport { convertTextElement } from \"./text-inference\";\n\n/** Tags handled elsewhere but recognised, so they never hit the unknown-tag arm. */\nconst STRUCTURAL_TAGS = new Set([\n \"mjml\",\n \"mj-head\",\n \"mj-body\",\n \"mj-wrapper\",\n \"mj-section\",\n \"mj-column\",\n \"mj-group\",\n \"mj-attributes\",\n \"mj-all\",\n \"mj-class\",\n \"mj-font\",\n \"mj-style\",\n \"mj-title\",\n \"mj-preview\",\n \"mj-breakpoint\",\n \"mj-html-attributes\",\n \"mj-social-element\",\n \"mj-navbar-link\",\n]);\n\n/** Tags with no Templatical equivalent that keep their markup verbatim. */\nconst NO_EQUIVALENT_TAGS = new Set([\"mj-hero\", \"mj-carousel\", \"mj-accordion\"]);\n\nfunction convertImage(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Block | null {\n const src = (attrs.src ?? \"\").trim();\n if (!src) return null;\n\n const decorative = (attrs.role ?? \"\").trim().toLowerCase() === \"presentation\";\n const pxWidth = parsePxValue(attrs.width);\n const height = parsePxValue(attrs.height);\n const borderRadius = parsePxValue(attrs[\"border-radius\"]);\n const href = (attrs.href ?? \"\").trim();\n\n return createImageBlock({\n src,\n alt: decorative ? \"\" : (attrs.alt ?? \"\"),\n // A px width equal to the container is how `width: \"full\"` renders\n // (renderer/src/renderers/image.ts), so restore the flag rather than\n // freezing the number — otherwise a full-width image stops resizing with\n // the template.\n width: pxWidth === ctx.containerWidth ? \"full\" : pxWidth || \"full\",\n align: parseAlignment(attrs.align, \"center\"),\n ...(height > 0 ? { height } : {}),\n ...(borderRadius > 0 ? { borderRadius } : {}),\n ...(href ? { linkUrl: href } : {}),\n ...(href && isNewTab(attrs) ? { linkOpenInNewTab: true } : {}),\n ...(decorative ? { decorative: true } : {}),\n ...baseFields(attrs),\n });\n}\n\nfunction convertButton($el: Cheerio<Element>, attrs: Attrs): Block | null {\n const text = ($el.text() ?? \"\").trim();\n if (!text) return null;\n\n const backgroundColor = parseColor(attrs[\"background-color\"]);\n const textColor = parseColor(attrs.color);\n const fontSize = parsePxValue(attrs[\"font-size\"]);\n\n return createButtonBlock({\n text,\n url: (attrs.href ?? \"\").trim(),\n ...(backgroundColor ? { backgroundColor } : {}),\n ...(textColor ? { textColor } : {}),\n ...(fontSize > 0 ? { fontSize } : {}),\n ...(attrs[\"border-radius\"] !== undefined\n ? { borderRadius: parsePxValue(attrs[\"border-radius\"]) }\n : {}),\n ...(attrs[\"inner-padding\"] !== undefined\n ? { buttonPadding: parsePaddingShorthand(attrs[\"inner-padding\"]) }\n : {}),\n align: parseAlignment(attrs.align, \"center\"),\n ...(isNewTab(attrs) ? { openInNewTab: true } : {}),\n ...baseFields(attrs),\n });\n}\n\n/**\n * `mj-divider`'s `width` as `DividerBlock.width`. A percentage is a share of\n * the column and stays one, to two decimals within 0–100%. `100%`, and a px\n * width that reaches `lineWidth`, span the line's whole box, which is\n * `\"full\"`. A clamped or unreadable value pushes a note.\n *\n * It reads a sign, which `parsePercent` does not, so a negative value is\n * clamped to 0 and reported instead of being dropped.\n */\nfunction readDividerWidth(\n value: string | undefined,\n lineWidth: number,\n notes: string[],\n): DividerBlock[\"width\"] {\n const raw = (value ?? \"\").trim();\n if (!raw) return \"full\";\n\n const match = /^(-?\\d+(?:\\.\\d+)?)\\s*(px|%)?$/i.exec(raw);\n if (!match) {\n notes.push(\n `Divider width \"${raw}\" could not be read; imported as full width.`,\n );\n return \"full\";\n }\n const parsed = parseFloat(match[1]);\n\n if (match[2] === \"%\") {\n const percent = Math.min(100, Math.max(0, parsed));\n if (percent !== parsed) {\n notes.push(`Divider width ${raw} was clamped to ${percent}%.`);\n }\n // Two decimals keep the value inside `DividerPercentWidth`'s pattern,\n // which a float printed in exponent form would leave.\n const share = Math.round(percent * 100) / 100;\n return share === 100 ? \"full\" : `${share}%`;\n }\n\n const px = Math.max(0, Math.round(parsed));\n if (px !== Math.round(parsed)) {\n notes.push(`Divider width ${raw} was clamped to 0px.`);\n }\n return px >= lineWidth ? \"full\" : px;\n}\n\n/**\n * `mj-divider` draws `100%` across its column less the column's side padding\n * and its own, so that is the width a px value is measured against. It places\n * a partial-width line by `align`, centred by default; Templatical centres\n * every divider, so a left- or right-aligned partial one comes back with a\n * note.\n */\nfunction convertDivider(attrs: Attrs, ctx: ConvertContext): Converted {\n const color = parseColor(attrs[\"border-color\"]);\n const thickness = parsePxValue(attrs[\"border-width\"]);\n const base = baseFields(attrs);\n const { left, right } = base.styles.padding;\n\n const notes: string[] = [];\n const width = readDividerWidth(\n attrs.width,\n ctx.containerWidth - ctx.columnPadding - left - right,\n notes,\n );\n\n const align = (attrs.align ?? \"\").trim().toLowerCase();\n if (width !== \"full\" && (align === \"left\" || align === \"right\")) {\n notes.push(\n `MJML aligns this divider ${align}; Templatical centres every divider.`,\n );\n }\n\n return {\n block: createDividerBlock({\n lineStyle: parseBorderStyle(attrs[\"border-style\"]),\n width,\n ...(color ? { color } : {}),\n ...(attrs[\"border-width\"] !== undefined ? { thickness } : {}),\n ...base,\n }),\n entry: {\n sourceTag: \"mj-divider\",\n templaticalBlockType: \"divider\",\n status: notes.length > 0 ? \"approximated\" : \"converted\",\n ...(notes.length > 0 ? { note: notes.join(\" \") } : {}),\n },\n };\n}\n\nfunction convertSpacer(attrs: Attrs): Block {\n const height = parsePxValue(attrs.height);\n\n return createSpacerBlock({\n ...(attrs.height !== undefined ? { height } : {}),\n ...baseFields(attrs),\n });\n}\n\n/**\n * Convert one MJML element to a Templatical block.\n *\n * Returns `null` for an element that produces nothing *and* warrants no report\n * entry — an image with no `src`, a button with no label. A `Converted` whose\n * `block` is null is a *skip*, which does get an entry.\n */\nexport function convertElement(\n $el: Cheerio<Element>,\n ctx: ConvertContext,\n): Converted | null {\n const tag = tagOf($el[0]);\n if (!tag) return null;\n\n const attrs = resolveAttributes($el, ctx.cascade);\n warnForeignClasses(attrs, tag, ctx);\n\n if (tag === \"mj-include\") {\n const path = (attrs.path ?? \"\").trim();\n return {\n block: null,\n entry: {\n sourceTag: tag,\n templaticalBlockType: null,\n status: \"skipped\",\n note: `Cannot resolve <mj-include path=\"${path}\"> — the importer reads a single string and has no filesystem access. Inline the include before importing.`,\n },\n };\n }\n\n if (tag === \"mj-text\") {\n return convertTextElement($el, attrs, ctx);\n }\n\n if (tag === \"mj-social\") {\n return convertSocial($el, attrs, ctx);\n }\n\n if (tag === \"mj-navbar\") {\n return convertNavbar($el, attrs, ctx);\n }\n\n if (tag === \"mj-table\") {\n return convertNativeTable($el, attrs, ctx);\n }\n\n if (tag === \"mj-image\") {\n const block = convertImage($el, attrs, ctx);\n if (!block) return null;\n return {\n block,\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"image\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"mj-button\") {\n const block = convertButton($el, attrs);\n if (!block) return null;\n return {\n block,\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"button\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"mj-divider\") {\n return convertDivider(attrs, ctx);\n }\n\n if (tag === \"mj-spacer\") {\n return {\n block: convertSpacer(attrs),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"spacer\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"mj-raw\") {\n return {\n block: createHtmlBlock({\n content: $el.html() ?? \"\",\n ...baseFields(attrs),\n }),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"converted\",\n },\n };\n }\n\n if (NO_EQUIVALENT_TAGS.has(tag)) {\n return {\n block: convertHtmlFallback($el, ctx, attrs),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `<${tag}> has no Templatical block equivalent; the original markup is preserved.`,\n },\n };\n }\n\n if (STRUCTURAL_TAGS.has(tag)) return null;\n\n return {\n block: convertHtmlFallback($el, ctx, attrs),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `<${tag}> is not a known MJML element (a custom component?); the original markup is preserved.`,\n },\n };\n}\n","import type { Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport { SYNTAX_PRESETS } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport { tagOf } from \"./attribute-resolver\";\n\ntype DisplayCondition = NonNullable<Block[\"displayCondition\"]>;\n\nexport interface SiblingUnit {\n $el: Cheerio<Element>;\n displayCondition?: DisplayCondition;\n}\n\n/** Characters kept before an ellipsis is appended; the full condition stays in `before`. */\nconst LABEL_MAX = 46;\n\nconst ANCHORED_LOGIC = Object.values(SYNTAX_PRESETS).map(\n (preset) =>\n new RegExp(\n `^(?:${preset.logic.source})$`,\n preset.logic.flags.replace(\"g\", \"\"),\n ),\n);\n\n/**\n * Whether the text is exactly one logic tag and nothing else.\n *\n * Anchored against every registered syntax rather than one, so a mailchimp or\n * ampscript template is recognised as readily as a liquid one. MSO conditionals\n * are HTML comments and match none of them, which is what keeps hand-written\n * `mj-raw` pairs out of this path.\n */\nexport function isLogicTagOnly(text: string): boolean {\n const trimmed = text.trim();\n if (trimmed === \"\") return false;\n return ANCHORED_LOGIC.some((regex) => regex.test(trimmed));\n}\n\nfunction isLogicRaw($el: Cheerio<Element>): boolean {\n return tagOf($el[0]) === \"mj-raw\" && isLogicTagOnly($el.text() ?? \"\");\n}\n\nfunction synthesizeLabel(before: string): string {\n const trimmed = before.trim();\n if (trimmed.length <= LABEL_MAX) return trimmed;\n return `${trimmed.slice(0, LABEL_MAX)}…`;\n}\n\n/**\n * Group a run of siblings into units, folding each `logic-raw / element /\n * logic-raw` triple into one unit carrying a `displayCondition`.\n *\n * `label` is synthesised from `before`: it is editor metadata that appears\n * nowhere in the MJML, so it cannot be recovered — only reconstructed. `group`\n * and `description` are left absent for the same reason.\n *\n * Deliberately no check that the two tags pair *semantically*. The renderer\n * emits them only in this arrangement, and matching open/close keywords across\n * four syntaxes would be a table to maintain for no gain.\n */\nexport function planSiblings($siblings: Cheerio<Element>[]): SiblingUnit[] {\n const units: SiblingUnit[] = [];\n let i = 0;\n\n while (i < $siblings.length) {\n const $current = $siblings[i];\n\n const $middle = $siblings[i + 1];\n const $closing = $siblings[i + 2];\n\n const isTriple =\n isLogicRaw($current) &&\n $middle !== undefined &&\n tagOf($middle[0]) !== \"mj-raw\" &&\n $closing !== undefined &&\n isLogicRaw($closing);\n\n if (isTriple) {\n const before = ($current.text() ?? \"\").trim();\n const after = ($closing.text() ?? \"\").trim();\n units.push({\n $el: $middle,\n displayCondition: { label: synthesizeLabel(before), before, after },\n });\n i += 3;\n continue;\n }\n\n units.push({ $el: $current });\n i += 1;\n }\n\n return units;\n}\n","import type { CheerioAPI } from \"cheerio\";\nimport {\n DEFAULT_TEMPLATE_DEFAULTS,\n resolveContentDirection,\n} from \"@templatical/types\";\nimport type {\n ContentDirection,\n TemplateContent,\n TemplateSettings,\n} from \"@templatical/types\";\nimport { parseColor, parseFontFamily, parsePxValue } from \"./attribute-parser\";\nimport {\n childElements,\n findByTag,\n tagOf,\n type AttributeCascade,\n} from \"./attribute-resolver\";\n\n/** `mj-head` children this module reads; anything else is warned about. */\nconst CONSUMED_HEAD_TAGS = new Set([\n \"mj-attributes\",\n \"mj-preview\",\n \"mj-font\",\n \"mj-style\",\n \"mj-title\",\n]);\n\n/**\n * `DEFAULT_TEMPLATE_DEFAULTS` is typed `Partial<TemplateSettings>` so a\n * consumer can override any subset of it, but its own literal (see\n * `packages/types/src/defaults.ts`) always sets exactly the six required\n * `TemplateSettings` fields — `linkColor` and `preheaderText` are the two\n * optional ones and are correctly absent from it. Narrowing the type once\n * here, rather than at every read below, is what lets `width`, `fontFamily`\n * and the rest come out as `number`/`string`/`boolean` instead of `| undefined`.\n */\nconst REQUIRED_TEMPLATE_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\ninterface AnchorRule {\n color?: string;\n underline?: boolean;\n}\n\n/**\n * Read the `a { … }` declarations out of the concatenated `mj-style` blocks.\n *\n * This is the reverse of how the renderer emits `settings.linkColor` and\n * `settings.linkUnderline` — as a global anchor rule — so a template that\n * round-trips keeps both. A stylesheet with no anchor rule leaves both unset.\n */\nfunction readAnchorRule(css: string): AnchorRule {\n const rule: AnchorRule = {};\n\n // Strip CSS comments to avoid breaking property matching when comments\n // precede or separate declarations (e.g. `a { /* note */ color: #0055ff; }`).\n const cssWithoutComments = css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n\n for (const match of cssWithoutComments.matchAll(\n /(^|[},])\\s*a\\s*\\{([^}]*)\\}/g,\n )) {\n const body = match[2];\n\n const color = body.match(/(?:^|;)\\s*color\\s*:\\s*([^;]+)/i);\n if (color) {\n const parsed = parseColor(color[1]);\n if (parsed) rule.color = parsed;\n }\n\n const decoration = body.match(/(?:^|;)\\s*text-decoration\\s*:\\s*([^;]+)/i);\n if (decoration) {\n rule.underline = decoration[1].trim().toLowerCase().includes(\"underline\");\n }\n }\n\n return rule;\n}\n\n/**\n * `<mjml dir>` as `settings.direction`, kept only when it disagrees with the\n * direction the locale implies. The renderer always writes `dir`, resolved\n * from the locale when a template states no direction, so keeping a value that\n * agrees would add a key the source template never set. `auto`, MJML's\n * default, states no direction.\n */\nfunction readDirection(\n value: string | undefined,\n locale: string,\n): ContentDirection | undefined {\n const dir = (value ?? \"\").trim().toLowerCase();\n if (dir !== \"ltr\" && dir !== \"rtl\") return undefined;\n return dir === resolveContentDirection({ locale }) ? undefined : dir;\n}\n\n/**\n * Build `TemplateSettings` from the `mjml` and `mj-body` attributes, the\n * attribute cascade and the remaining `mj-head` children.\n *\n * Optional keys (`preheaderText`, `linkColor`, `direction`) are **omitted**\n * rather than set to `undefined`: an absent key is what the block model means\n * by unset, and a present-but-undefined key serialises into exported JSON.\n */\nexport function extractSettings(\n $: CheerioAPI,\n cascade: AttributeCascade,\n warnings: string[],\n): TemplateContent[\"settings\"] {\n const $body = findByTag($, \"mj-body\").first();\n const $root = findByTag($, \"mjml\").first();\n\n const width =\n parsePxValue($body.attr(\"width\")) || REQUIRED_TEMPLATE_DEFAULTS.width;\n const backgroundColor =\n parseColor($body.attr(\"background-color\")) ||\n REQUIRED_TEMPLATE_DEFAULTS.backgroundColor;\n\n const fontFromCascade =\n parseFontFamily(cascade.all[\"font-family\"]) ||\n parseFontFamily(cascade.byTag[\"mj-text\"]?.[\"font-family\"]);\n const fontFromDeclaration =\n findByTag($, \"mj-font\").first().attr(\"name\") ?? \"\";\n const fontFamily =\n fontFromCascade ||\n fontFromDeclaration ||\n REQUIRED_TEMPLATE_DEFAULTS.fontFamily;\n\n const textColor =\n parseColor(cascade.byTag[\"mj-text\"]?.color) ||\n REQUIRED_TEMPLATE_DEFAULTS.textColor;\n\n const previewText = findByTag($, \"mj-preview\").first().text().trim();\n\n // Every <mj-style> block in the document is pooled into one stylesheet before\n // the anchor rule is read, so a bare `a { … }` rule in any of them contributes.\n // Against the renderer's own output that is unambiguous: richTextStylesheet()\n // scopes every rule it emits to `p`, `ul`, `ol` and `li`, so the one bare\n // `a { … }` rule present is the document-level rule this function reads back.\n const styleCss = findByTag($, \"mj-style\")\n .toArray()\n .map((el) => $(el).text())\n .join(\"\\n\");\n const anchor = readAnchorRule(styleCss);\n\n const locale =\n ($root.attr(\"lang\") ?? \"\").trim() || REQUIRED_TEMPLATE_DEFAULTS.locale;\n const direction = readDirection($root.attr(\"dir\"), locale);\n\n const title = findByTag($, \"mj-title\").first().text().trim();\n if (title) {\n warnings.push(\n `Dropped <mj-title> (\"${title}\") — Templatical templates have no document-title field.`,\n );\n }\n\n const $head = findByTag($, \"mj-head\").first();\n if ($head.length > 0) {\n for (const $child of childElements($head, $)) {\n const tag = tagOf($child[0]);\n if (!CONSUMED_HEAD_TAGS.has(tag)) {\n warnings.push(`Dropped <${tag}> — it has no Templatical equivalent.`);\n }\n }\n }\n\n return {\n width,\n backgroundColor,\n textColor,\n linkUnderline: anchor.underline ?? REQUIRED_TEMPLATE_DEFAULTS.linkUnderline,\n fontFamily,\n locale,\n ...(direction ? { direction } : {}),\n ...(anchor.color ? { linkColor: anchor.color } : {}),\n ...(previewText ? { preheaderText: previewText } : {}),\n };\n}\n","import type { Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport { createSectionBlock } from \"@templatical/types\";\nimport type {\n Block,\n ColumnLayout,\n SectionWrapper,\n SpacingValue,\n} from \"@templatical/types\";\nimport {\n parseColor,\n parseDefinitePx,\n parsePaddingShorthand,\n parsePercent,\n parsePxValue,\n} from \"./attribute-parser\";\nimport {\n childElements,\n resolveAttributes,\n tagOf,\n type Attrs,\n} from \"./attribute-resolver\";\nimport { convertElement } from \"./block-mapper\";\nimport {\n baseFields,\n convertHtmlFallback,\n type ConvertContext,\n} from \"./block-base\";\nimport { planSiblings } from \"./display-condition\";\nimport type { ImportReportEntry } from \"./types\";\n\n/** Reverse of `packages/renderer/src/columns.ts`, keyed by column count. */\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\n/** Percentage points of drift tolerated per column before a match is inexact. */\nconst WIDTH_TOLERANCE = 2;\n\n/**\n * Resolve MJML column widths to one of the five layouts `ColumnLayout` allows.\n *\n * `exact: false` means the caller must report `approximated` — MJML permits any\n * number of columns at any width and this union permits five shapes, so this is\n * the importer's main irreducible loss (§8.1).\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 // No widths at all is MJML's \"distribute equally\", which is exactly what the\n // n-column 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 // A partial mix of known and absent widths fills each absent column with\n // its share of what the known widths leave over, not an equal split of the\n // whole row — an equal split is only correct when every column is absent,\n // which the branch above already handles. Clamped at 0 so a document whose\n // explicit widths already exceed 100% cannot produce a negative fill.\n const nullCount = percents.filter((p) => p === null).length;\n const knownSum = percents.reduce((sum: number, p) => sum + (p ?? 0), 0);\n const remainder = nullCount > 0 ? Math.max(0, 100 - knownSum) / nullCount : 0;\n const resolved = percents.map((p) => p ?? remainder);\n\n const sameCount = LAYOUT_SHAPES.filter(\n (shape) => shape.percents.length === count,\n );\n\n for (const shape of sameCount) {\n const fits = shape.percents.every(\n (want, i) => Math.abs(want - resolved[i]) <= WIDTH_TOLERANCE,\n );\n if (fits) return { layout: shape.layout, exact: true };\n }\n\n // Nearest same-count shape by total absolute error; if the count itself has\n // no shape (4+), collapse to \"3\" and let the caller fold the overflow.\n const candidates =\n sameCount.length > 0\n ? sameCount\n : LAYOUT_SHAPES.filter((shape) => shape.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\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\n/** Column pixel widths per layout, mirroring `renderer/src/columns.ts`. */\nfunction columnPixels(layout: ColumnLayout, containerWidth: number): number[] {\n switch (layout) {\n case \"2\":\n return [containerWidth * 0.5, containerWidth * 0.5];\n case \"3\":\n return [containerWidth / 3, containerWidth / 3, containerWidth / 3];\n case \"1-2\":\n return [containerWidth / 3, (containerWidth * 2) / 3];\n case \"2-1\":\n return [(containerWidth * 2) / 3, containerWidth / 3];\n default:\n return [containerWidth];\n }\n}\n\n/**\n * The `mj-column` elements of a section, in document order: a direct\n * `mj-column` child contributes itself, and an `mj-group` child contributes\n * every `mj-column` it holds at the group's own position — so a section\n * mixing direct columns with one or more groups keeps every column instead\n * of losing all but the first group's. `grouped` is true whenever any\n * `mj-group` child is present, which is what drives `stackOnMobile: false`.\n */\nfunction readColumns(\n $el: Cheerio<Element>,\n ctx: ConvertContext,\n): { columns: Cheerio<Element>[]; grouped: boolean } {\n const kids = childElements($el, ctx.$);\n const columns: Cheerio<Element>[] = [];\n let grouped = false;\n\n for (const $kid of kids) {\n const tag = tagOf($kid[0]);\n if (tag === \"mj-column\") {\n columns.push($kid);\n } else if (tag === \"mj-group\") {\n grouped = true;\n columns.push(\n ...childElements($kid, ctx.$).filter(\n ($k) => tagOf($k[0]) === \"mj-column\",\n ),\n );\n }\n }\n\n return { columns, grouped };\n}\n\n/**\n * A column's children, in document order, one block per unit `planSiblings`\n * groups them into.\n *\n * Routed through `planSiblings` for the same reason `walkBody` is\n * (`converter.ts`): the renderer wraps a conditional block in bracketing\n * `mj-raw` guards inside a column exactly as it does at top level\n * (`renderers/section.ts` calls the same `wrapWithDisplayCondition` helper as\n * `index.ts`), so recovering the condition here needs the identical fold.\n */\nfunction convertColumnChildren(\n $column: Cheerio<Element>,\n ctx: ConvertContext,\n entries: ImportReportEntry[],\n): Block[] {\n const blocks: Block[] = [];\n\n for (const unit of planSiblings(childElements($column, ctx.$))) {\n const tag = tagOf(unit.$el[0]);\n\n // MJML forbids mj-section inside mj-column, and `addBlock` in\n // @templatical/core refuses a section into a column too, so producing one\n // here would be dropped downstream without a trace.\n if (tag === \"mj-section\" || tag === \"mj-wrapper\") {\n const attrs = resolveAttributes(unit.$el, ctx.cascade);\n const fallback = convertHtmlFallback(unit.$el, ctx, attrs);\n if (unit.displayCondition)\n fallback.displayCondition = unit.displayCondition;\n blocks.push(fallback);\n const noun = tag === \"mj-section\" ? \"section\" : \"wrapper\";\n entries.push({\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `MJML forbids <${tag}> inside <mj-column>; the nested ${noun}'s markup is preserved as an html block.`,\n });\n continue;\n }\n\n const converted = convertElement(unit.$el, ctx);\n if (!converted) continue;\n entries.push(converted.entry);\n if (!converted.block) continue;\n\n if (unit.displayCondition) {\n converted.block.displayCondition = unit.displayCondition;\n }\n\n blocks.push(converted.block);\n }\n\n return blocks;\n}\n\n/**\n * A column's width as a percentage of the section's container, or `null`\n * when the column carries no definite width of its own.\n *\n * `mj-column` accepts either a percentage or a px length, and a px width is\n * real geometry — converting it here lets it participate in\n * `matchColumnLayout` as a known value instead of falling through to \"auto\"\n * and losing the author's intended ratio.\n */\nfunction columnWidthPercent(\n value: string | undefined,\n containerWidth: number,\n): number | null {\n const percent = parsePercent(value);\n if (percent !== null) return percent;\n\n const px = parseDefinitePx(value);\n return px !== null && containerWidth > 0 ? (px / containerWidth) * 100 : null;\n}\n\n/**\n * Build a `SectionBlock` (always exactly one) from an `mj-section`.\n *\n * Returns an array so the caller can treat sections and wrappers uniformly.\n */\nexport function buildSection(\n $el: Cheerio<Element>,\n ctx: ConvertContext,\n entries: ImportReportEntry[],\n wrapper?: SectionWrapper,\n): Block[] {\n const attrs = resolveAttributes($el, ctx.cascade);\n const { columns, grouped } = readColumns($el, ctx);\n\n const columnAttrs = columns.map(($c) => resolveAttributes($c, ctx.cascade));\n const rawWidths = columnAttrs.map((column) => column.width);\n const percents = rawWidths.map((width) =>\n columnWidthPercent(width, ctx.containerWidth),\n );\n const { layout, exact } = matchColumnLayout(percents);\n const slots = COLUMN_COUNT[layout];\n const pixels = columnPixels(layout, ctx.containerWidth);\n\n // `entries` is the document-wide report the whole walk shares, not a\n // per-section array, so the section's own entry is pushed here — before its\n // children convert — to land ahead of them and ahead of whatever a sibling\n // section pushes next. Splicing it in afterward (e.g. `unshift`) would put\n // every section's entry at the front of the entire report instead of at the\n // front of its own children, reversing section order in a multi-section\n // document and interleaving children under the wrong parent.\n //\n // Built from the raw attribute strings, not the resolved `percents` — a px\n // width would otherwise show as a repeating-decimal percentage of the\n // container instead of the value the author actually wrote.\n const shown = rawWidths.map((w) => w || \"auto\").join(\", \");\n entries.push({\n sourceTag: \"mj-section\",\n templaticalBlockType: \"section\",\n status: exact ? \"converted\" : \"approximated\",\n ...(exact\n ? {}\n : {\n note: `Column widths ${shown} have no exact Templatical layout; resolved to \"${layout}\".`,\n }),\n });\n\n const children: Block[][] = Array.from({ length: slots }, () => []);\n\n columns.forEach(($column, index) => {\n // A 4th+ column folds into the last slot rather than becoming an html\n // block: its content converts perfectly and only the geometry is lost.\n const slot = Math.min(index, slots - 1);\n const { left, right } = parsePaddingShorthand(columnAttrs[index].padding);\n const columnCtx: ConvertContext = {\n ...ctx,\n containerWidth: Math.round(pixels[slot] ?? ctx.containerWidth),\n columnPadding: left + right,\n };\n children[slot].push(...convertColumnChildren($column, columnCtx, entries));\n });\n\n const borderRadius = parsePxValue(attrs[\"border-radius\"]);\n\n const section = createSectionBlock({\n columns: layout,\n children,\n ...(grouped ? { stackOnMobile: false } : {}),\n ...(borderRadius > 0 ? { borderRadius } : {}),\n ...(wrapper ? { wrapper } : {}),\n // \"native\": mj-section is the one element whose own background-color\n // attribute is its container fill (renderers/section.ts:32) — every other\n // baseFields caller reads container-background-color instead.\n ...baseFields(attrs, \"native\"),\n });\n\n return [section];\n}\n\nfunction readWrapper(attrs: Attrs): SectionWrapper {\n const backgroundColor = parseColor(attrs[\"background-color\"]);\n const padding: SpacingValue = parsePaddingShorthand(attrs.padding);\n const borderRadius = parsePxValue(attrs[\"border-radius\"]);\n\n return {\n ...(backgroundColor ? { backgroundColor } : {}),\n padding,\n ...(borderRadius > 0 ? { borderRadius } : {}),\n };\n}\n\n/**\n * Fold an `mj-wrapper` into the `wrapper` field of the section(s) it holds.\n *\n * A wrapper is not a block: `SectionWrapper` is exactly the band the renderer\n * emits an `mj-wrapper` for (`renderer/src/index.ts:223`), so representing it\n * as its own section would double the nesting on every round trip.\n */\nexport function buildWrapper(\n $el: Cheerio<Element>,\n ctx: ConvertContext,\n entries: ImportReportEntry[],\n): Block[] {\n const attrs = resolveAttributes($el, ctx.cascade);\n const wrapper = readWrapper(attrs);\n\n const sections = childElements($el, ctx.$).filter(\n ($k) => tagOf($k[0]) === \"mj-section\",\n );\n\n if (sections.length === 0) {\n entries.push({\n sourceTag: \"mj-wrapper\",\n templaticalBlockType: null,\n status: \"skipped\",\n note: \"An <mj-wrapper> with no <mj-section> children produces nothing.\",\n });\n return [];\n }\n\n if (sections.length > 1) {\n entries.push({\n sourceTag: \"mj-wrapper\",\n templaticalBlockType: \"section\",\n status: \"approximated\",\n note: `An <mj-wrapper> holding ${sections.length} sections was applied to each of them — Templatical has no multi-section band.`,\n });\n }\n\n return sections.flatMap(($section) =>\n buildSection($section, ctx, entries, wrapper),\n );\n}\n","import { load } from \"cheerio\";\nimport type { Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport {\n createDefaultTemplateContent,\n createSectionBlock,\n resolveContentDirection,\n} from \"@templatical/types\";\nimport type { Block, TemplateContent } from \"@templatical/types\";\nimport {\n buildAttributeCascade,\n childElements,\n findByTag,\n tagOf,\n} from \"./attribute-resolver\";\nimport { convertElement } from \"./block-mapper\";\nimport type { ConvertContext } from \"./block-base\";\nimport { planSiblings } from \"./display-condition\";\nimport { extractSettings } from \"./head-parser\";\nimport { buildSection, buildWrapper } from \"./section-builder\";\nimport type { ImportReport, ImportReportEntry, ImportResult } from \"./types\";\n\nconst EMPTY_DOCUMENT_WARNING =\n \"No convertible content was found in the MJML. Check that the document has an <mj-body> with at least one <mj-section>.\";\n\n/**\n * Wrap blocks that sat directly under `mj-body` in a one-column section.\n *\n * Valid MJML puts every block inside an `mj-section`, but hand-written and\n * machine-mangled documents do not, and the editor canvas has no\n * representation for a block outside a section.\n */\nfunction wrapInSection(blocks: Block[]): Block {\n return createSectionBlock({\n columns: \"1\",\n children: [blocks],\n styles: { padding: { top: 0, right: 0, bottom: 0, left: 0 } },\n });\n}\n\nfunction walkBody(\n $body: Cheerio<Element>,\n ctx: ConvertContext,\n entries: ImportReportEntry[],\n): Block[] {\n const blocks: Block[] = [];\n let loose: Block[] = [];\n\n const flushLoose = () => {\n if (loose.length > 0) {\n blocks.push(wrapInSection(loose));\n loose = [];\n }\n };\n\n for (const unit of planSiblings(childElements($body, ctx.$))) {\n const tag = tagOf(unit.$el[0]);\n\n if (tag === \"mj-wrapper\" || tag === \"mj-section\") {\n flushLoose();\n const produced =\n tag === \"mj-wrapper\"\n ? buildWrapper(unit.$el, ctx, entries)\n : buildSection(unit.$el, ctx, entries);\n\n // A condition brackets one element, so it lands on every block that\n // element produced — which is more than one only for a multi-section\n // wrapper, where each band carries the same guard.\n for (const block of produced) {\n if (unit.displayCondition)\n block.displayCondition = unit.displayCondition;\n blocks.push(block);\n }\n continue;\n }\n\n const converted = convertElement(unit.$el, ctx);\n if (!converted) continue;\n entries.push(converted.entry);\n if (!converted.block) continue;\n\n if (unit.displayCondition) {\n converted.block.displayCondition = unit.displayCondition;\n }\n\n // An html fallback for a body-level element is already a top-level block;\n // wrapping it in a section would add nesting the source never had.\n if (converted.entry.status === \"html-fallback\") {\n flushLoose();\n blocks.push(converted.block);\n continue;\n }\n\n loose.push(converted.block);\n }\n\n flushLoose();\n return blocks;\n}\n\n/**\n * Convert an MJML document into a Templatical template.\n *\n * @example\n * ```ts\n * const { content, report } = convertMjmlTemplate(mjmlSource);\n *\n * const editor = init({ container: '#editor', content });\n *\n * console.log(report.summary);\n * console.log(report.warnings);\n * ```\n */\nexport function convertMjmlTemplate(mjml: string): ImportResult {\n if (typeof mjml !== \"string\") {\n throw new Error(\n \"Invalid MJML template: expected a string. Pass the raw MJML source as a string.\",\n );\n }\n if (mjml.trim().length === 0) {\n throw new Error(\n \"Invalid MJML template: input is empty. Pass the raw MJML source of an email.\",\n );\n }\n\n // The `xml` option routes parsing through htmlparser2 rather than parse5, so\n // custom `mj-*` tags survive as generic elements and no implicit\n // <html><head><body> is injected around them. Two htmlparser2 options are\n // then overridden away from what `xml: true` alone would give:\n //\n // - `xmlMode: false` puts htmlparser2 in HTML mode, which knows the void\n // element list (`br`, `img`, `hr`, …) and closes them immediately. In XML\n // mode a bare `<br>` stays open and everything after it — including the\n // rest of the paragraph — becomes ITS CHILD rather than its sibling. That\n // shape is exactly what TipTap emits for a hard break and what browser DOM\n // serialization produces, so it hits `<mj-text>` content routinely; a\n // later HTML-mode reparse (e.g. loading the block into the editor) then\n // invents a *second* `<br>` from the dangling `</br>`, per the HTML5 rule\n // that a stray `<br>` end tag opens a new `<br>` rather than closing one.\n // - `recognizeSelfClosing: true` restores what HTML mode otherwise gives up:\n // without it, a self-closing custom tag like `<mj-image src=\"…\" />` never\n // actually closes, and swallows whatever follows as its child instead of\n // its sibling — verified against a self-closing `<mj-all />` immediately\n // followed by a sibling `<mj-text />` inside `<mj-attributes>`, which\n // nested the second tag inside the first and silently dropped a per-tag\n // attribute default.\n const $ = load(mjml, { xml: { xmlMode: false, recognizeSelfClosing: true } });\n const cascade = buildAttributeCascade($);\n\n const entries: ImportReportEntry[] = [];\n const warnings: string[] = [];\n\n const settings = extractSettings($, cascade, warnings);\n\n const $body = findByTag($, \"mj-body\").first();\n const ctx: ConvertContext = {\n $,\n cascade,\n containerWidth: settings.width,\n columnPadding: 0,\n direction: resolveContentDirection(settings),\n warnings,\n };\n\n const blocks = $body.length > 0 ? walkBody($body, ctx, entries) : [];\n\n if (blocks.length === 0) {\n warnings.push(EMPTY_DOCUMENT_WARNING);\n }\n\n const content: TemplateContent = {\n ...createDefaultTemplateContent(),\n blocks,\n 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"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/attribute-resolver.ts","../src/attribute-parser.ts","../src/block-base.ts","../src/composite-mapper.ts","../src/text-inference.ts","../src/block-mapper.ts","../src/display-condition.ts","../src/head-parser.ts","../src/section-builder.ts","../src/converter.ts"],"sourcesContent":["import type { Cheerio, CheerioAPI } from \"cheerio\";\nimport type { AnyNode, Element } from \"domhandler\";\nimport type { BlockVisibility } from \"@templatical/types\";\n\nexport type Attrs = Record<string, string>;\n\n/**\n * The flattened contents of `mj-head > mj-attributes`, built once per document.\n */\nexport interface AttributeCascade {\n /** From `<mj-all …>` — applies to every element. */\n all: Attrs;\n /** From `<mj-text …>` etc. — keyed by lowercased tag name. */\n byTag: Record<string, Attrs>;\n /** From `<mj-class name=\"x\" …>` — keyed by class name. */\n byClass: Record<string, Attrs>;\n}\n\n/**\n * A node's tag name, lowercased.\n *\n * The parser configuration this package uses (`xmlMode: false`, set in\n * `converter.ts`) already lowercases every tag at parse time, so this call is\n * a no-op on that parser's output. It remains the one place every tag\n * comparison in this package goes through, so a comparison stays correct\n * regardless of the active parser configuration — a bare `$(\"mj-body\")` would\n * otherwise miss a document that shouts its tags.\n */\nexport function tagOf(node: AnyNode | undefined): string {\n if (!node) return \"\";\n return (node as Element).tagName?.toLowerCase() ?? \"\";\n}\n\n/**\n * Every element with the given tag name, matched case-insensitively.\n */\nexport function findByTag($: CheerioAPI, tag: string): Cheerio<Element> {\n const wanted = tag.toLowerCase();\n return $(\"*\").filter(\n (_, el) => tagOf(el) === wanted,\n ) as unknown as Cheerio<Element>;\n}\n\n/**\n * An element's element children.\n *\n * `.children()` already excludes text and comment nodes; the tag filter here\n * guards against a node whose `tagName` is undefined, not against either of\n * those.\n */\nexport function childElements(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n): Cheerio<Element>[] {\n return $el\n .children()\n .toArray()\n .filter((node) => tagOf(node) !== \"\")\n .map((node) => $(node) as unknown as Cheerio<Element>);\n}\n\nfunction attrsOf($el: Cheerio<Element>): Attrs {\n const raw = $el.attr();\n if (!raw) return {};\n const out: Attrs = {};\n for (const [key, value] of Object.entries(raw)) {\n out[key.toLowerCase()] = value;\n }\n return out;\n}\n\n/**\n * Read `mj-head > mj-attributes` into the three buckets the cascade resolves\n * against. Called once per document; `resolveAttributes` is then pure lookup.\n */\nexport function buildAttributeCascade($: CheerioAPI): AttributeCascade {\n // Every bucket is a lookup table keyed by names taken from the imported\n // document, so a class or tag literally named `__proto__` must not be able\n // to reach the bucket's own prototype — `Object.create(null)` means a\n // computed-key write like `cascade.byClass[name] = …` can only ever create\n // an own property, never reassign what the bucket inherits from.\n const cascade: AttributeCascade = {\n all: Object.create(null),\n byTag: Object.create(null),\n byClass: Object.create(null),\n };\n\n const containers = findByTag($, \"mj-attributes\").toArray();\n for (const container of containers) {\n const $container = $(container) as unknown as Cheerio<Element>;\n for (const $child of childElements($container, $)) {\n const tag = tagOf($child[0]);\n const attrs = attrsOf($child);\n\n if (tag === \"mj-all\") {\n Object.assign(cascade.all, attrs);\n continue;\n }\n\n if (tag === \"mj-class\") {\n const { name, ...rest } = attrs;\n if (!name) continue;\n cascade.byClass[name] = { ...(cascade.byClass[name] ?? {}), ...rest };\n continue;\n }\n\n cascade.byTag[tag] = { ...(cascade.byTag[tag] ?? {}), ...attrs };\n }\n }\n\n return cascade;\n}\n\n/**\n * The first two cascade layers alone — `mj-all` then the per-tag default —\n * with neither a named `mj-class` nor the element's own inline attributes on\n * top. This is the ambient value a tag inherits before the element sets\n * anything of its own, which is what {@link ownAttr} compares against.\n */\nfunction resolveTagDefaults(tag: string, cascade: AttributeCascade): Attrs {\n return {\n ...cascade.all,\n ...(cascade.byTag[tag] ?? {}),\n };\n}\n\n/**\n * An element's effective attributes, highest precedence last:\n * `mj-all` → per-tag default → each named `mj-class` → the element's own\n * inline attributes.\n *\n * MJML's built-in component defaults are deliberately not modelled — the block\n * factories in `@templatical/types` supply that floor instead, which keeps this\n * module from becoming a copy of MJML's component table.\n */\nexport function resolveAttributes(\n $el: Cheerio<Element>,\n cascade: AttributeCascade,\n): Attrs {\n const own = attrsOf($el);\n const tag = tagOf($el[0]);\n\n const resolved: Attrs = resolveTagDefaults(tag, cascade);\n\n const classNames = (own[\"mj-class\"] ?? \"\")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n for (const name of classNames) {\n Object.assign(resolved, cascade.byClass[name] ?? {});\n }\n\n Object.assign(resolved, own);\n delete resolved[\"mj-class\"];\n\n return resolved;\n}\n\n/**\n * A resolved attribute value, but only when the element is the reason it has\n * that value — a named `mj-class` counts as the element opting in, but a bare\n * `mj-all`/per-tag cascade default does not.\n *\n * This reverses the renderer's inherit-by-omission convention for `mj-text`'s\n * `color` and `font-family`: `renderers/title.ts`, `table.ts` and `menu.ts`\n * all emit that attribute only when the block sets its own value, otherwise\n * leaving the element to inherit the document default from\n * `<mj-attributes>` (`renderers/title.ts:31`). `resolveAttributes` flattens\n * that default onto every element of the tag regardless, so reading `attrs`\n * directly cannot tell \"the block set this\" from \"the document default\n * reached this element too\" — and treating the latter as the former invents\n * a per-block override the source never made.\n *\n * `resolved === ambient` also covers an element that repeats the ambient\n * value on purpose: the two cases render identically, so which one happened\n * is not observable in the output and dropping it costs nothing (the same\n * reasoning as §8.3b's image-width and §8.4b's paragraph-gap defaults).\n */\nexport function ownAttr(\n attrs: Attrs,\n key: string,\n tag: string,\n cascade: AttributeCascade,\n): string | undefined {\n const resolved = attrs[key];\n if (resolved === undefined) return undefined;\n return resolved === resolveTagDefaults(tag, cascade)[key]\n ? undefined\n : resolved;\n}\n\nconst HIDE_DESKTOP = \"tpl-hide-desktop\";\nconst HIDE_MOBILE = \"tpl-hide-mobile\";\n\n/**\n * Marks a rendered title or paragraph's rich-text spacing — mirrors\n * `RICH_TEXT_CSS_CLASS` in `packages/renderer/src/rich-text.ts`. `title.ts`\n * and `paragraph.ts` pass it as a second argument to `getCssClassAttr`, so\n * every rendered title and paragraph carries this class on `css-class`\n * alongside any visibility markers.\n */\nconst RICH_TEXT_CSS_CLASS = \"tpl-rich-text\";\n\n/**\n * Matches the per-block paragraph-gap class the same two renderers append,\n * e.g. `tpl-rich-text-8` (`richTextGapClass` in `rich-text.ts`). The gap\n * accepts a decimal (`tpl-rich-text-8.5`) because `ParagraphBlock.paragraphSpacing`\n * is a plain `number` that a headless caller can set to a fractional value,\n * even though the editor UI clamps it to an integer. Negative gaps are out of\n * scope — the renderer never emits one, so there is nothing here that needs to\n * recognise it.\n *\n * Shared by `readForeignCssClasses` (which classes to exclude) and\n * `readParagraphGap` (which class to decode), so the two can never disagree\n * about what a gap class looks like.\n */\nconst RICH_TEXT_GAP_CLASS = /^tpl-rich-text-(\\d+(?:\\.\\d+)?)$/;\n\n/**\n * Marks a rendered full-width button — mirrors `BUTTON_FULL_WIDTH_CSS_CLASS`\n * in `packages/renderer/src/renderers/button.ts`. It carries no meaning of its\n * own: `width=\"100%\"` on the same `mj-button` already says the button is\n * full-width.\n */\nconst BUTTON_FULL_WIDTH_CSS_CLASS = \"tpl-button-full\";\n\nfunction cssClasses(attrs: Attrs): string[] {\n return (attrs[\"css-class\"] ?? \"\").trim().split(/\\s+/).filter(Boolean);\n}\n\n/**\n * Reverse of the renderer's `getCssClassAttr` (`packages/renderer/src/visibility.ts`).\n * `css-class` also carries the renderer's other markers (`RICH_TEXT_CSS_CLASS`,\n * `RICH_TEXT_GAP_CLASS`, `BUTTON_FULL_WIDTH_CSS_CLASS`) — this function reads\n * only the two visibility classes off it and leaves those alone.\n *\n * Returns `undefined` when neither class is present — absence means visible\n * everywhere, and writing `{ desktop: true, mobile: true }` instead would put a\n * redundant key in every imported block.\n */\nexport function readVisibility(attrs: Attrs): BlockVisibility | undefined {\n const classes = cssClasses(attrs);\n const hideDesktop = classes.includes(HIDE_DESKTOP);\n const hideMobile = classes.includes(HIDE_MOBILE);\n\n if (!hideDesktop && !hideMobile) return undefined;\n\n return { desktop: !hideDesktop, mobile: !hideMobile };\n}\n\n/**\n * The paragraph gap encoded on a rendered title or paragraph's `css-class` —\n * reverse of `richTextGapClass` (`packages/renderer/src/rich-text.ts`), e.g.\n * `8` from `tpl-rich-text-8`.\n *\n * Returns `null` when no gap class is present. Callers must not confuse that\n * with a gap of `0`, which is a legitimate value in its own right.\n */\nexport function readParagraphGap(attrs: Attrs): number | null {\n for (const name of cssClasses(attrs)) {\n const match = RICH_TEXT_GAP_CLASS.exec(name);\n if (match) return parseFloat(match[1]);\n }\n return null;\n}\n\n/**\n * Classes on `css-class` that carry no Templatical meaning. The caller warns\n * about these rather than dropping them silently: they are consumer CSS with no\n * home in the block model, and a template that relies on them will render\n * differently after import.\n *\n * Excludes the renderer's own rich-text and full-width button markers\n * alongside the two visibility classes, so importing a template the renderer\n * itself produced does not report one of its own classes as foreign. A\n * consumer's own `tpl-`-prefixed class is not one of these markers and is\n * still reported.\n */\nexport function readForeignCssClasses(attrs: Attrs): string[] {\n return cssClasses(attrs).filter(\n (name) =>\n name !== HIDE_DESKTOP &&\n name !== HIDE_MOBILE &&\n name !== RICH_TEXT_CSS_CLASS &&\n name !== BUTTON_FULL_WIDTH_CSS_CLASS &&\n !RICH_TEXT_GAP_CLASS.test(name),\n );\n}\n","import type {\n BorderRadiusValue,\n BorderSideValue,\n SpacingValue,\n} from \"@templatical/types\";\n\n/**\n * Parses a px-like MJML attribute value (`\"12px\"`, `\"12\"`, `12`) into a rounded\n * integer. Returns 0 for missing or unparseable input, and for units the block\n * model cannot express (em, rem, %) — a caller that needs to tell \"absent\" from\n * \"0\" must check the raw attribute itself.\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 match = value.match(/^\\s*(-?\\d+(?:\\.\\d+)?)\\s*(?:px)?\\s*$/);\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 * Normalizes a colour value to a 6-digit lowercase hex string.\n *\n * Returns `\"\"` for transparent/inherit/none and for anything unrecognised.\n * The empty string is the block model's \"unset\" — the colour pickers clear to\n * it — so returning 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\n if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed;\n\n if (/^#[0-9a-f]{3}$/.test(trimmed)) {\n const r = trimmed[1];\n const g = trimmed[2];\n const b = trimmed[3];\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n\n const rgbMatch = trimmed.match(\n /^rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(?:,\\s*[\\d.]+\\s*)?\\)$/,\n );\n if (rgbMatch) {\n return rgbToHex(\n parseInt(rgbMatch[1], 10),\n parseInt(rgbMatch[2], 10),\n parseInt(rgbMatch[3], 10),\n );\n }\n\n if (NAMED_COLORS[trimmed]) return NAMED_COLORS[trimmed];\n\n return \"\";\n}\n\n/**\n * Parses an MJML `padding` shorthand (1-4 values, CSS order) into a\n * SpacingValue.\n */\nexport function parsePaddingShorthand(value: string | undefined): SpacingValue {\n if (!value) return { top: 0, right: 0, bottom: 0, left: 0 };\n\n const values = value\n .trim()\n .split(/\\s+/)\n .map((p) => parsePxValue(p));\n\n switch (values.length) {\n case 1:\n return {\n top: values[0],\n right: values[0],\n bottom: values[0],\n left: values[0],\n };\n case 2:\n return {\n top: values[0],\n right: values[1],\n bottom: values[0],\n left: values[1],\n };\n case 3:\n return {\n top: values[0],\n right: values[1],\n bottom: values[2],\n left: values[1],\n };\n default:\n return {\n top: values[0],\n right: values[1],\n bottom: values[2],\n left: values[3],\n };\n }\n}\n\n/**\n * Parses an alignment to one of the three the block model accepts.\n */\nexport function parseAlignment(\n value: string | undefined,\n fallback: \"left\" | \"center\" | \"right\" = \"left\",\n): \"left\" | \"center\" | \"right\" {\n const v = (value ?? \"\").trim().toLowerCase();\n if (v === \"left\" || v === \"center\" || v === \"right\") return v;\n return fallback;\n}\n\n/**\n * Reads a percentage value, or `null` when the value is not a percentage.\n *\n * `null` rather than a number, because column-width matching (§8.1) has to tell\n * \"no percentage given\" from \"0%\" — the former distributes widths equally, the\n * latter is a real (if degenerate) width.\n */\nexport function parsePercent(value: string | undefined): number | null {\n if (!value) return null;\n const match = value.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*%$/);\n return match ? parseFloat(match[1]) : null;\n}\n\n/**\n * Reads a definite px length, or `null` when the value is not one.\n *\n * Unlike `parsePxValue`, which returns `0` for anything unparseable, this\n * tells \"no length given\" apart from \"0px\" — column-width recovery (§8.1)\n * needs that distinction for a px `mj-column` width the same way it needs\n * `parsePercent`'s `null` for a percentage one, so the two compose into a\n * single known-or-absent value the matcher can fill around.\n */\nexport function parseDefinitePx(value: string | undefined): number | null {\n if (!value) return null;\n const match = value.trim().match(/^(-?\\d+(?:\\.\\d+)?)\\s*(?:px)?$/);\n return match ? parseFloat(match[1]) : null;\n}\n\nconst PX_LENGTH = /^(\\d+(?:\\.\\d+)?)(?:px)?$/i;\n\n/**\n * Reads a `border-radius` value: one px length, or the two- to four-value\n * shorthand (top-left, top-right, bottom-right, bottom-left), expanded the way\n * CSS expands it. Four matching corners come back as one number, the form the\n * renderer writes them in. `null` when any part is not a non-negative px\n * length (`50%`, `1em`, an elliptical `8px / 4px`), which CSS either reads\n * against the box or discards.\n */\nexport function parseBorderRadius(value: string): BorderRadiusValue | null {\n const parts = value.trim().split(/\\s+/);\n if (parts.length > 4) return null;\n\n const px: number[] = [];\n for (const part of parts) {\n const match = PX_LENGTH.exec(part);\n if (!match) return null;\n px.push(Math.round(parseFloat(match[1])));\n }\n\n const [\n topLeft,\n topRight = topLeft,\n bottomRight = topLeft,\n bottomLeft = topRight,\n ] = px;\n if (\n topRight === topLeft &&\n bottomRight === topLeft &&\n bottomLeft === topLeft\n ) {\n return topLeft;\n }\n return { topLeft, topRight, bottomRight, bottomLeft };\n}\n\nconst DRAWN_STYLES = new Set([\"solid\", \"dashed\", \"dotted\"]);\nconst NARROWED_STYLES = new Set([\n \"double\",\n \"groove\",\n \"ridge\",\n \"inset\",\n \"outset\",\n]);\nconst UNDRAWN_STYLES = new Set([\"none\", \"hidden\"]);\n\n/**\n * Splits a CSS value on whitespace outside parentheses, so `rgb(0, 0, 0)` stays\n * one token. A linear scan rather than a regex, which would backtrack on a long\n * run of spaces.\n */\nfunction splitCssTokens(value: string): string[] {\n const tokens: string[] = [];\n let current = \"\";\n let depth = 0;\n for (const ch of value) {\n if (ch === \"(\") depth++;\n if (ch === \")\") depth = Math.max(0, depth - 1);\n if (depth === 0 && /\\s/.test(ch)) {\n if (current) tokens.push(current);\n current = \"\";\n continue;\n }\n current += ch;\n }\n if (current) tokens.push(current);\n return tokens;\n}\n\n/**\n * Reads one CSS `border` declaration, its width, style and colour in any\n * order, as a `BorderSideValue`.\n *\n * `side` is `null` when CSS draws nothing: style `none` or `hidden`, no style\n * at all (CSS's initial style is `none`), or a `0` width. A style the block\n * model lacks narrows to solid and is named in `narrowedFrom`. A missing or\n * unknown colour is `\"\"`, which the renderer writes back as no colour, so CSS\n * draws `currentColor` both times.\n *\n * Returns `null` for a drawn border it cannot hold: a width that is not px\n * (`2em`, `thin`), no width at all (CSS draws `medium`), or a value with two\n * of any part.\n */\nexport function parseBorderDeclaration(\n value: string,\n): { side: BorderSideValue | null; narrowedFrom?: string } | null {\n let width: number | undefined;\n let style: string | undefined;\n let color: string | undefined;\n\n for (const token of splitCssTokens(value.trim())) {\n const lower = token.toLowerCase();\n const px = PX_LENGTH.exec(lower);\n const isStyle =\n DRAWN_STYLES.has(lower) ||\n NARROWED_STYLES.has(lower) ||\n UNDRAWN_STYLES.has(lower);\n\n if (px) {\n if (width !== undefined) return null;\n width = Math.round(parseFloat(px[1]));\n } else if (isStyle) {\n if (style !== undefined) return null;\n style = lower;\n } else if (/^[-+.\\d]/.test(lower) || /^(thin|medium|thick)$/.test(lower)) {\n return null;\n } else {\n if (color !== undefined) return null;\n color = token;\n }\n }\n\n if (style === undefined || UNDRAWN_STYLES.has(style) || width === 0) {\n return { side: null };\n }\n if (width === undefined) return null;\n\n const side: BorderSideValue = {\n width,\n style: NARROWED_STYLES.has(style)\n ? \"solid\"\n : (style as BorderSideValue[\"style\"]),\n color: parseColor(color),\n };\n return NARROWED_STYLES.has(style) ? { side, narrowedFrom: style } : { side };\n}\n\n/**\n * Narrows a border style to the three `DividerBlock.lineStyle` accepts.\n */\nexport function parseBorderStyle(\n value: string | undefined,\n): \"solid\" | \"dashed\" | \"dotted\" {\n const v = (value ?? \"\").trim().toLowerCase();\n if (v === \"dashed\" || v === \"dotted\") return v;\n return \"solid\";\n}\n","import type { Cheerio, CheerioAPI } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport { BORDER_SIDES, createHtmlBlock } from \"@templatical/types\";\nimport type {\n Block,\n BlockStyles,\n BlockVisibility,\n BorderRadiusValue,\n BorderSide,\n BorderSideValue,\n BorderValue,\n ContentDirection,\n} from \"@templatical/types\";\nimport {\n parseBorderDeclaration,\n parseBorderRadius,\n parseColor,\n parsePaddingShorthand,\n} from \"./attribute-parser\";\nimport {\n readForeignCssClasses,\n readVisibility,\n type Attrs,\n type AttributeCascade,\n} from \"./attribute-resolver\";\nimport type { ImportReportEntry } from \"./types\";\n\n/**\n * Everything a converter needs beyond the element itself.\n *\n * `containerWidth` is the width the element renders at — a section's column\n * width, or `settings.width` at top level. It is what lets an `mj-image` whose\n * px width equals its container restore `width: \"full\"` (§8.3b), and what an\n * `mj-divider`'s px width is measured against, less `columnPadding` and the\n * divider's own side padding.\n *\n * `columnPadding` is the side padding (left plus right, px) of the\n * `mj-column` holding the element, 0 at top level. A Templatical column has no\n * padding of its own, so this is read only where it changes what a value\n * means: the width an `mj-divider` line can span.\n *\n * `direction` is the writing direction the imported template resolves to. Its\n * start edge is where a paragraph sits when its markup states no alignment,\n * so an `mj-text` aligned to that edge needs none.\n */\nexport interface ConvertContext {\n $: CheerioAPI;\n cascade: AttributeCascade;\n containerWidth: number;\n columnPadding: number;\n direction: ContentDirection;\n warnings: string[];\n}\n\n/** A produced block plus its report entry. `block` is null only when skipped. */\nexport interface Converted {\n block: Block | null;\n entry: ImportReportEntry;\n}\n\n/**\n * The `styles` and `visibility` every block shares.\n *\n * `placement` mirrors the renderer's own `BgPlacement` split\n * (`packages/renderer/src/utils.ts`): `mj-section` is the one \"native\"\n * element, whose own `background-color` attribute *is* `styles.backgroundColor`\n * (`renderers/section.ts:32`). Every other block type emits\n * `container-background-color` for that same field, because a plain\n * `background-color` on those tags already means something else of the\n * element's own — a button's fill (`renderers/button.ts:43`), for one — and\n * reading it back as the block's container background would invent a fill\n * the source never had. Defaulting to `\"container\"` matches every caller but\n * `buildSection`.\n *\n * `visibility` is spread conditionally so an unset block carries no key — the\n * block model treats absence as \"visible everywhere\".\n */\nexport function baseFields(\n attrs: Attrs,\n placement: \"container\" | \"native\" = \"container\",\n): {\n styles: BlockStyles;\n visibility?: BlockVisibility;\n} {\n const bgKey =\n placement === \"native\" ? \"background-color\" : \"container-background-color\";\n const backgroundColor = parseColor(attrs[bgKey]);\n const visibility = readVisibility(attrs);\n\n return {\n styles: {\n padding: parsePaddingShorthand(attrs.padding),\n ...(backgroundColor ? { backgroundColor } : {}),\n },\n ...(visibility ? { visibility } : {}),\n };\n}\n\n/**\n * `border-radius` as a `BorderRadiusValue`, `undefined` when the attribute is\n * absent. A value with no px reading is square corners, `0`, and pushes a\n * note: `label` names the element in it.\n */\nexport function readBorderRadius(\n attrs: Attrs,\n label: string,\n notes: string[],\n): BorderRadiusValue | undefined {\n const raw = attrs[\"border-radius\"];\n if (raw === undefined) return undefined;\n\n const radius = parseBorderRadius(raw);\n if (radius === null) {\n notes.push(\n `${label} border-radius \"${raw.trim()}\" could not be read; imported as square corners.`,\n );\n return 0;\n }\n return radius;\n}\n\n/**\n * What the editor's border control holds for a side with no width\n * (`BorderControl.vue`), so a border drawn on some sides reads back the way\n * the editor would have stored it.\n */\nconst UNDRAWN_SIDE: BorderSideValue = {\n width: 0,\n style: \"solid\",\n color: \"#000000\",\n};\n\n/**\n * `border` and `border-<side>` as a `BorderValue`, `undefined` when no side is\n * drawn. A side takes its own `border-<side>` over `border`, since MJML writes\n * the side after the shorthand. A declaration that a side actually uses pushes\n * a note when its style narrows to solid or it cannot be read, in which case\n * that side is left undrawn.\n */\nexport function readBorder(\n attrs: Attrs,\n label: string,\n notes: string[],\n): BorderValue | undefined {\n const read = new Map<string, BorderSideValue | null>();\n const sides = {} as Record<BorderSide, BorderSideValue | null>;\n\n for (const side of BORDER_SIDES) {\n const name =\n attrs[`border-${side}`] !== undefined\n ? `border-${side}`\n : attrs.border !== undefined\n ? \"border\"\n : null;\n if (name === null) {\n sides[side] = null;\n continue;\n }\n\n if (!read.has(name)) {\n const raw = attrs[name]!.trim();\n const parsed = parseBorderDeclaration(raw);\n if (parsed === null) {\n notes.push(\n `${label} ${name} \"${raw}\" could not be read; imported without it.`,\n );\n } else if (parsed.narrowedFrom) {\n notes.push(\n `${label} ${name} \"${raw}\" uses style ${parsed.narrowedFrom}; imported as solid.`,\n );\n }\n read.set(name, parsed?.side ?? null);\n }\n const parsed = read.get(name)!;\n sides[side] = parsed && { ...parsed };\n }\n\n if (BORDER_SIDES.every((side) => sides[side] === null)) return undefined;\n\n return {\n top: sides.top ?? { ...UNDRAWN_SIDE },\n right: sides.right ?? { ...UNDRAWN_SIDE },\n bottom: sides.bottom ?? { ...UNDRAWN_SIDE },\n left: sides.left ?? { ...UNDRAWN_SIDE },\n };\n}\n\nexport function warnForeignClasses(\n attrs: Attrs,\n tag: string,\n ctx: ConvertContext,\n): void {\n for (const name of readForeignCssClasses(attrs)) {\n ctx.warnings.push(\n `Dropped CSS class \"${name}\" on <${tag}> — consumer CSS has no Templatical equivalent.`,\n );\n }\n}\n\nexport function isNewTab(attrs: Attrs): boolean {\n return (attrs.target ?? \"\").trim().toLowerCase() === \"_blank\";\n}\n\n/**\n * Wrap the element's own markup in an HTML block — the lossless fallback.\n */\nexport function convertHtmlFallback(\n $el: Cheerio<Element>,\n ctx: ConvertContext,\n attrs: Attrs,\n): Block {\n const outer = ctx.$.html($el) ?? \"\";\n return createHtmlBlock({ content: outer, ...baseFields(attrs) });\n}\n","import type { Cheerio } from \"cheerio\";\nimport {\n canonicalFontStack,\n createMenuBlock,\n createSocialIconsBlock,\n createTableBlock,\n generateId,\n SOCIAL_ICON_TONES,\n SOCIAL_ICON_STYLES,\n} from \"@templatical/types\";\nimport type {\n MenuItemData,\n SocialIcon,\n SocialIconTone,\n SocialIconSize,\n SocialIconStyle,\n SocialPlatform,\n TableRowData,\n} from \"@templatical/types\";\nimport type { Element } from \"domhandler\";\nimport { parseAlignment, parseColor, parsePxValue } from \"./attribute-parser\";\nimport {\n childElements,\n ownAttr,\n resolveAttributes,\n tagOf,\n type Attrs,\n} from \"./attribute-resolver\";\nimport { baseFields, type ConvertContext, type Converted } from \"./block-base\";\n\n/**\n * Exhaustive over `SocialPlatform` on purpose: adding a member to that union\n * without adding it here is a compile error, so the importer cannot silently\n * fall back 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 PLATFORM_ALIASES: Record<string, SocialPlatform> = {\n x: \"twitter\",\n \"x-twitter\": \"twitter\",\n};\n\n/**\n * Icon packs (and MJML's `-noshare` variants) append tokens to the platform\n * slug: `facebook-round-outlined.png`, `facebook-noshare`. Those stems are not\n * `SocialPlatform` values, so a lookup that stops at the raw filename maps\n * the icon to `\"website\"` and the platform name is lost.\n */\nconst PLATFORM_PACK_TOKENS = new Set([\n \"noshare\",\n \"round\",\n \"outlined\",\n \"square\",\n \"circle\",\n \"solid\",\n]);\n\nfunction lookupPlatform(cleaned: string): SocialPlatform | null {\n if (PLATFORM_ALIASES[cleaned]) return PLATFORM_ALIASES[cleaned];\n return cleaned in KNOWN_PLATFORMS ? (cleaned as SocialPlatform) : null;\n}\n\nfunction normalizePlatform(raw: string): SocialPlatform | null {\n const cleaned = raw.trim().toLowerCase();\n if (!cleaned) return null;\n\n const direct = lookupPlatform(cleaned);\n if (direct) return direct;\n\n const parts = cleaned.split(\"-\");\n while (\n parts.length > 1 &&\n PLATFORM_PACK_TOKENS.has(parts[parts.length - 1])\n ) {\n parts.pop();\n const candidate = parts.join(\"-\");\n const hit = lookupPlatform(candidate);\n if (hit) return hit;\n }\n return null;\n}\n\n/** The `<dir>/<platform>.png` tail of the URL `renderers/social.ts:79` builds. */\nfunction platformFromSrc(src: string): { platform: string; style: string } {\n const parts = src.split(\"?\")[0].split(\"/\").filter(Boolean);\n const file = parts.at(-1) ?? \"\";\n return {\n platform: file.replace(/\\.[a-z0-9]+$/i, \"\"),\n style: parts.at(-2) ?? \"\",\n };\n}\n\nconst ICON_SIZES: Array<[number, SocialIconSize]> = [\n [24, \"small\"],\n [32, \"medium\"],\n [48, \"large\"],\n];\n\nfunction nearestIconSize(px: number): { size: SocialIconSize; exact: boolean } {\n let best = ICON_SIZES[1];\n let bestGap = Infinity;\n for (const candidate of ICON_SIZES) {\n const gap = Math.abs(candidate[0] - px);\n if (gap < bestGap) {\n bestGap = gap;\n best = candidate;\n }\n }\n return { size: best[1], exact: bestGap === 0 };\n}\n\nconst RADIUS_STYLES: Record<string, SocialIconStyle> = {\n \"50%\": \"circle\",\n \"8px\": \"rounded\",\n \"0\": \"square\",\n \"4px\": \"solid\",\n};\n\n/**\n * The style and tone in an icon folder the renderer names: `circle` for a\n * brand-colored circle, `circle-dark` for a dark one (`socialIconAssetDir`).\n */\nfunction iconDirParts(\n dir: string,\n): { style: SocialIconStyle; tone?: SocialIconTone } | null {\n const [style, tone, ...rest] = dir.split(\"-\");\n if (rest.length > 0 || !SOCIAL_ICON_STYLES.includes(style as SocialIconStyle))\n return null;\n if (tone === undefined) return { style: style as SocialIconStyle };\n return SOCIAL_ICON_TONES.includes(tone as SocialIconTone) && tone !== \"brand\"\n ? { style: style as SocialIconStyle, tone: tone as SocialIconTone }\n : null;\n}\n\nexport function convertSocial(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted | null {\n const elements = childElements($el, ctx.$).filter(\n ($child) => tagOf($child[0]) === \"mj-social-element\",\n );\n if (elements.length === 0) return null;\n\n const notes: string[] = [];\n const icons: SocialIcon[] = [];\n\n let iconStyle: SocialIconStyle | null = null;\n let iconTone: SocialIconTone | undefined;\n let iconSizePx = 0;\n let spacing = 0;\n\n elements.forEach(($child, index) => {\n const childAttrs = resolveAttributes($child, ctx.cascade);\n const src = (childAttrs.src ?? \"\").trim();\n const fromSrc = src ? platformFromSrc(src) : { platform: \"\", style: \"\" };\n\n const name = (childAttrs.name ?? \"\").trim();\n const alt = (childAttrs.alt ?? \"\").trim();\n const platform =\n normalizePlatform(name) ??\n normalizePlatform(fromSrc.platform) ??\n normalizePlatform(alt);\n if (!platform) {\n const rawName = name || fromSrc.platform || alt;\n if (rawName) {\n notes.push(\n `Unrecognised social platform \"${rawName}\" mapped to \"website\".`,\n );\n }\n }\n\n icons.push({\n id: generateId(),\n platform: platform ?? \"website\",\n url: (childAttrs.href ?? \"\").trim(),\n });\n\n const fromDir = iconStyle ? null : iconDirParts(fromSrc.style);\n if (fromDir) {\n iconStyle = fromDir.style;\n iconTone = fromDir.tone;\n }\n\n if (iconSizePx === 0) {\n iconSizePx = parsePxValue(childAttrs[\"icon-size\"]);\n }\n\n // The final element emits `0` right-padding, so spacing is only readable\n // from a non-final one (renderers/social.ts:82).\n if (spacing === 0 && index < elements.length - 1) {\n spacing = parsePxValue((childAttrs.padding ?? \"\").trim().split(/\\s+/)[1]);\n }\n\n if (!iconStyle) {\n const radius = (childAttrs[\"border-radius\"] ?? \"\").trim().toLowerCase();\n const mapped = RADIUS_STYLES[radius];\n if (mapped) {\n iconStyle = mapped;\n if (radius === \"4px\") {\n notes.push(\n 'Icon border-radius 4px maps to both \"solid\" and \"outlined\"; resolved to \"solid\".',\n );\n }\n }\n }\n });\n\n const declaredSize = parsePxValue(attrs[\"icon-size\"]) || iconSizePx;\n let iconSize: SocialIconSize | undefined;\n if (declaredSize > 0) {\n const resolved = nearestIconSize(declaredSize);\n iconSize = resolved.size;\n if (!resolved.exact) {\n notes.push(\n `Icon size ${declaredSize}px is not one of 24/32/48; resolved to \"${resolved.size}\".`,\n );\n }\n }\n\n const block = createSocialIconsBlock({\n icons,\n align: parseAlignment(attrs.align, \"center\"),\n ...(iconSize ? { iconSize } : {}),\n ...(iconStyle ? { iconStyle } : {}),\n ...(iconTone ? { iconTone } : {}),\n ...(spacing > 0 ? { spacing } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\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\nexport function convertNavbar(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted | null {\n const links = childElements($el, ctx.$).filter(\n ($child) => tagOf($child[0]) === \"mj-navbar-link\",\n );\n if (links.length === 0) return null;\n\n const items: MenuItemData[] = links.map(($link) => {\n const linkAttrs = resolveAttributes($link, ctx.cascade);\n // ownAttr, not linkAttrs directly — same cascade-vs-own distinction as\n // the block-level font-family read below: a hand-written document's own\n // <mj-attributes><mj-navbar-link color=\"…\"/></mj-attributes> default must\n // not be read back as this link's own override.\n const color = parseColor(\n ownAttr(linkAttrs, \"color\", \"mj-navbar-link\", ctx.cascade),\n );\n return {\n id: generateId(),\n text: ($link.text() ?? \"\").trim(),\n url: (linkAttrs.href ?? \"\").trim(),\n openInNewTab: (linkAttrs.target ?? \"\").toLowerCase() === \"_blank\",\n bold: (linkAttrs[\"font-weight\"] ?? \"\").toLowerCase() === \"bold\",\n underline: (linkAttrs[\"text-decoration\"] ?? \"\").includes(\"underline\"),\n ...(color ? { color } : {}),\n };\n });\n\n const fontSize = parsePxValue(attrs[\"font-size\"]);\n // ownAttr, not attrs directly — same cascade-vs-own distinction ownAttr\n // documents (attribute-resolver.ts): our own renderer never emits\n // mj-navbar, but a hand-written document's own\n // <mj-attributes><mj-navbar font-family=\"…\"/></mj-attributes> default must\n // not be read back as this navbar's own override.\n const fontFamily = canonicalFontStack(\n ownAttr(attrs, \"font-family\", \"mj-navbar\", ctx.cascade),\n );\n\n const block = createMenuBlock({\n items,\n textAlign: parseAlignment(attrs.align, \"center\"),\n ...(fontSize > 0 ? { fontSize } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-navbar\",\n templaticalBlockType: \"menu\",\n status: \"converted\",\n },\n };\n}\n\nexport function convertNativeTable(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted | null {\n const $ = ctx.$;\n // .find(\"tr\") is a descendant search, so a <table> nested inside a cell\n // would otherwise contribute its own rows here too, interleaved with\n // $el's own. Keep only rows with no \"table\" ancestor between themselves\n // and $el — a nested table's rows stay untouched inside their cell's\n // content, read verbatim by the .html() call below.\n const rowEls = $el\n .find(\"tr\")\n .toArray()\n .filter((tr) => $(tr).parentsUntil($el, \"table\").length === 0);\n if (rowEls.length === 0) return null;\n\n const rows: TableRowData[] = rowEls.map((rowEl) => ({\n id: generateId(),\n cells: $(rowEl)\n .children()\n .toArray()\n .map((cellEl) => ({ id: generateId(), content: $(cellEl).html() ?? \"\" })),\n }));\n\n const hasHeaderRow = $(rowEls[0])\n .children()\n .toArray()\n .some((cell) => tagOf(cell) === \"th\");\n\n // ownAttr, not attrs directly — same cascade-vs-own distinction as\n // convertNavbar above: a hand-written document's own\n // <mj-attributes><mj-table color=\"…\" font-family=\"…\"/></mj-attributes>\n // default must not be read back as this table's own override.\n const color = parseColor(ownAttr(attrs, \"color\", \"mj-table\", ctx.cascade));\n const fontSize = parsePxValue(attrs[\"font-size\"]);\n const fontFamily = canonicalFontStack(\n ownAttr(attrs, \"font-family\", \"mj-table\", ctx.cascade),\n );\n\n const block = createTableBlock({\n rows,\n hasHeaderRow,\n textAlign: parseAlignment(attrs.align, \"left\"),\n ...(color ? { color } : {}),\n ...(fontSize > 0 ? { fontSize } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-table\",\n templaticalBlockType: \"table\",\n status: \"converted\",\n },\n };\n}\n","import { load } from \"cheerio\";\nimport type { Cheerio, CheerioAPI } from \"cheerio\";\nimport { isTag, isText } from \"domhandler\";\nimport type { AnyNode, Element } from \"domhandler\";\nimport {\n canonicalFontStack,\n createMenuBlock,\n createParagraphBlock,\n createTableBlock,\n createTitleBlock,\n generateId,\n RICH_TEXT_SPACING,\n} from \"@templatical/types\";\nimport type {\n HeadingLevel,\n MenuItemData,\n TableRowData,\n} from \"@templatical/types\";\nimport { parseAlignment, parseColor, parsePxValue } from \"./attribute-parser\";\nimport {\n ownAttr,\n readParagraphGap,\n type Attrs,\n type AttributeCascade,\n} from \"./attribute-resolver\";\nimport { baseFields, type ConvertContext, type Converted } from \"./block-base\";\n\n/**\n * Re-parse an `mj-text`'s inner markup into its own document.\n *\n * The surrounding document already parses void elements correctly (`br`,\n * `img`, … stay void under `converter.ts`'s `xmlMode: false`), so this reparse\n * isn't compensating for a different parser here. It gives each\n * shape-detection helper (title / table / menu) an isolated, freshly\n * queryable document scoped to just this block's own markup — so\n * `$inner(\"tr\")` can only match rows that belong to this table, and\n * `$inner(\"body\")` has a real root to enumerate top-level nodes from. A\n * paragraph's markup is reparsed only when an attribute has to be carried\n * into it (`styleParagraphs`); otherwise it passes through verbatim.\n */\nfunction parseInner(html: string) {\n return load(`<body>${html}</body>`);\n}\n\nfunction rootElements(html: string): { tag: string; count: number } {\n const $inner = parseInner(html);\n const kids = $inner(\"body\").children().toArray();\n return {\n tag: kids.length > 0 ? (kids[0].tagName?.toLowerCase() ?? \"\") : \"\",\n count: kids.length,\n };\n}\n\nconst HEADING_LEVELS: Record<string, number> = {\n h1: 1,\n h2: 2,\n h3: 3,\n h4: 4,\n h5: 5,\n h6: 6,\n};\n\nfunction convertTitle(\n html: string,\n attrs: Attrs,\n sourceLevel: number,\n cascade: AttributeCascade,\n): Converted {\n const $inner = parseInner(html);\n const $heading = $inner(\"body\").children().first();\n const level = Math.min(sourceLevel, 4) as HeadingLevel;\n // Read via ownAttr, not attrs directly: title.ts emits color/font-family\n // only when the block sets its own, so a value that only reached this\n // element through the document-wide `<mj-attributes>` cascade (§7) is not\n // this title's own and must not be read back onto it.\n const color = parseColor(ownAttr(attrs, \"color\", \"mj-text\", cascade));\n const fontFamily = canonicalFontStack(\n ownAttr(attrs, \"font-family\", \"mj-text\", cascade),\n );\n\n const block = createTitleBlock({\n content: $heading.html() ?? \"\",\n level,\n textAlign: parseAlignment(attrs.align, \"left\"),\n ...(color ? { color } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseFields(attrs),\n });\n\n const clamped = sourceLevel > 4;\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"title\",\n status: clamped ? \"approximated\" : \"converted\",\n ...(clamped\n ? {\n note: `Heading level h${sourceLevel} clamped to 4 — Templatical titles support h1-h4.`,\n }\n : {}),\n },\n };\n}\n\nfunction convertTable(\n html: string,\n attrs: Attrs,\n cascade: AttributeCascade,\n): Converted {\n const $inner = parseInner(html);\n const $rows = $inner(\"tr\");\n\n const rows: TableRowData[] = $rows.toArray().map((rowEl) => ({\n id: generateId(),\n cells: $inner(rowEl)\n .children()\n .toArray()\n .map((cellEl) => ({\n id: generateId(),\n content: $inner(cellEl).html() ?? \"\",\n })),\n }));\n\n const hasHeaderRow =\n $rows.length > 0 && $inner($rows[0]).children(\"th\").length > 0;\n\n // ownAttr, not attrs directly — same cascade-vs-own distinction as\n // convertTitle above: table.ts also emits color/font-family only when the\n // block sets its own (renderers/table.ts:34,29).\n const color = parseColor(ownAttr(attrs, \"color\", \"mj-text\", cascade));\n const fontSize = parsePxValue(attrs[\"font-size\"]);\n const fontFamily = canonicalFontStack(\n ownAttr(attrs, \"font-family\", \"mj-text\", cascade),\n );\n\n const block = createTableBlock({\n rows,\n hasHeaderRow,\n textAlign: parseAlignment(attrs.align, \"left\"),\n ...(color ? { color } : {}),\n ...(fontSize > 0 ? { fontSize } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"table\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * A menu is top-level anchors with optional `<span>` separators between them —\n * exactly what `renderers/menu.ts` emits, and deliberately not matched when a\n * `<p>` wrapper is present (that is a paragraph containing links).\n *\n * `.children()` is elements only. A non-whitespace text-node sibling must\n * veto: an `mj-text` of prose plus a trailing `<a>` becomes a menu labelled\n * from the links and the prose is discarded. Whitespace-only nodes (newlines\n * between `<a>`/`<span>`) must not.\n */\nfunction looksLikeMenu(html: string): boolean {\n const $inner = parseInner(html);\n const kids = $inner(\"body\").contents().toArray();\n if (kids.length === 0) return false;\n\n let anchors = 0;\n for (const kid of kids) {\n if (isText(kid)) {\n if (kid.data.trim() !== \"\") return false;\n continue;\n }\n if (!isTag(kid)) continue;\n const tag = kid.tagName.toLowerCase();\n if (tag === \"a\") anchors += 1;\n else if (tag !== \"span\") return false;\n }\n\n return anchors > 0;\n}\n\nfunction convertMenu(\n html: string,\n attrs: Attrs,\n cascade: AttributeCascade,\n): Converted {\n const $inner = parseInner(html);\n\n const items: MenuItemData[] = $inner(\"body\")\n .children(\"a\")\n .toArray()\n .map((el) => {\n const $a = $inner(el);\n const itemColor = parseColor(\n $a.attr(\"style\")?.match(/color\\s*:\\s*([^;]+)/i)?.[1],\n );\n return {\n id: generateId(),\n text: ($a.text() ?? \"\").trim(),\n url: $a.attr(\"href\") ?? \"\",\n openInNewTab: ($a.attr(\"target\") ?? \"\").toLowerCase() === \"_blank\",\n bold: $a.find(\"strong, b\").length > 0,\n underline: ($a.attr(\"style\") ?? \"\").includes(\"underline\"),\n ...(itemColor ? { color: itemColor } : {}),\n };\n });\n\n const $separator = $inner(\"body\").children(\"span\").first();\n const separator = ($separator.text() ?? \"\").trim();\n const separatorColor = parseColor(\n $separator.attr(\"style\")?.match(/color\\s*:\\s*([^;]+)/i)?.[1],\n );\n const spacing = parsePxValue(\n $separator.attr(\"style\")?.match(/padding\\s*:\\s*0\\s+([\\d.]+px)/i)?.[1],\n );\n\n // ownAttr, not attrs directly — same cascade-vs-own distinction as\n // convertTitle above: menu.ts also emits color/font-family only when the\n // block sets its own (renderers/menu.ts:29,24).\n const color = parseColor(ownAttr(attrs, \"color\", \"mj-text\", cascade));\n const fontSize = parsePxValue(attrs[\"font-size\"]);\n const fontFamily = canonicalFontStack(\n ownAttr(attrs, \"font-family\", \"mj-text\", cascade),\n );\n\n const block = createMenuBlock({\n items,\n textAlign: parseAlignment(attrs.align, \"center\"),\n ...(separator ? { separator } : {}),\n ...(separatorColor ? { separatorColor } : {}),\n ...(spacing > 0 ? { spacing } : {}),\n ...(color ? { color } : {}),\n ...(fontSize > 0 ? { fontSize } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"menu\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * The editor's rich-text blocks assume a block-level wrapper, so bare text gets\n * one. Mirrors `ensureParagraphWrapped` in `@templatical/import-html`.\n */\nfunction ensureParagraphWrapped(html: string): string {\n const trimmed = html.trim();\n if (trimmed === \"\") return \"<p></p>\";\n if (/^<(p|h[1-6]|ul|ol|blockquote|div)\\b/i.test(trimmed)) return trimmed;\n return `<p>${trimmed}</p>`;\n}\n\n/**\n * The size the renderer gives every `mj-text` (`<mj-text font-size=\"14px\">` in\n * `packages/renderer/src/index.ts`), so a paragraph at this size needs no span\n * to carry it.\n */\nconst RENDERER_TEXT_SIZE = 14;\n\nconst PARAGRAPH_ALIGNMENTS = new Set([\"left\", \"center\", \"right\", \"justify\"]);\n\n/** `<li>` children that end a run of the item's text instead of joining it. */\nconst LIST_ITEM_BLOCK_TAGS = new Set([\n \"p\",\n \"div\",\n \"ul\",\n \"ol\",\n \"blockquote\",\n \"table\",\n \"pre\",\n \"hr\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n]);\n\n/**\n * What an `mj-text` states about its paragraphs' alignment, colour and size.\n * `ParagraphBlock` has a field for none of them, so they travel in the markup.\n * An empty string states nothing.\n */\ninterface ParagraphStyle {\n textAlign: string;\n color: string;\n fontSize: string;\n}\n\nfunction readParagraphStyle(attrs: Attrs, ctx: ConvertContext): ParagraphStyle {\n // The start edge is where a paragraph sits with no alignment of its own, and\n // the renderer states it on every RTL paragraph (`renderers/paragraph.ts`).\n const align = (attrs.align ?? \"\").trim().toLowerCase();\n const startEdge = ctx.direction === \"rtl\" ? \"right\" : \"left\";\n\n // The resolved size, not ownAttr: a size set document-wide has no setting to\n // land in, so dropping it as ambient would lose it.\n const size = parsePxValue(attrs[\"font-size\"]);\n\n return {\n textAlign:\n PARAGRAPH_ALIGNMENTS.has(align) && align !== startEdge ? align : \"\",\n // A colour set document-wide is `settings.textColor` already.\n color: parseColor(ownAttr(attrs, \"color\", \"mj-text\", ctx.cascade)),\n fontSize: size > 0 && size !== RENDERER_TEXT_SIZE ? `${size}px` : \"\",\n };\n}\n\n/**\n * Put each run of an `<li>`'s inline content in a `<p>`, so that the\n * alignment has a paragraph to sit on.\n */\nfunction wrapListItemText($inner: CheerioAPI, li: Element): void {\n let run: AnyNode[] = [];\n const flush = () => {\n // wrapAll, not before() and append(): before() skips a text node, which\n // is what a run usually starts with, and the text would be lost.\n if (run.some((node) => isTag(node) || (isText(node) && node.data.trim()))) {\n $inner(run).wrapAll(\"<p></p>\");\n }\n run = [];\n };\n\n for (const child of [...li.children]) {\n if (isTag(child) && LIST_ITEM_BLOCK_TAGS.has(child.tagName.toLowerCase())) {\n flush();\n } else {\n run.push(child);\n }\n }\n flush();\n}\n\n/**\n * Align one `<p>` and wrap its content in a span carrying colour and size.\n * The `<p>`'s own `text-align` stays, and its own colour and size move into\n * the span, where they win over the attributes as they did on the `<p>`.\n */\nfunction styleParagraph(\n $inner: CheerioAPI,\n $p: Cheerio<Element>,\n style: ParagraphStyle,\n): void {\n if (style.textAlign && !$p.css(\"text-align\")) {\n $p.css(\"text-align\", style.textAlign);\n }\n\n const color = $p.css(\"color\") || style.color;\n const fontSize = $p.css(\"font-size\") || style.fontSize;\n if ((color || fontSize) && $p.contents().length > 0) {\n $p.css({ color: \"\", \"font-size\": \"\" });\n const $span = $inner(\"<span></span>\").css({\n ...(color ? { color } : {}),\n ...(fontSize ? { \"font-size\": fontSize } : {}),\n });\n $p.wrapInner($span);\n }\n\n if ($p.attr(\"style\") === \"\") $p.removeAttr(\"style\");\n}\n\n/**\n * Carry an `mj-text`'s alignment, colour and size into its paragraphs'\n * markup: `text-align` on each `<p>`, colour and size on one span inside it.\n * That is the shape the paragraph editor keeps through an edit\n * (`packages/editor/src/components/blocks/ParagraphEditor.vue` aligns\n * paragraphs and styles spans); a wrapper element around the paragraphs would\n * be dropped on the first edit.\n *\n * Markup with nothing to apply is returned as it came, unparsed.\n */\nfunction styleParagraphs(html: string, style: ParagraphStyle): string {\n if (!style.textAlign && !style.color && !style.fontSize) return html;\n\n const $inner = parseInner(html);\n $inner(\"li\").each((_, li) => wrapListItemText($inner, li));\n $inner(\"p\").each((_, p) => styleParagraph($inner, $inner(p), style));\n return $inner(\"body\").html() ?? html;\n}\n\nfunction convertParagraph(\n html: string,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted {\n // A custom gap round-trips through `css-class` as `tpl-rich-text-<gap>`\n // (`richTextGapClass` in `packages/renderer/src/rich-text.ts`); the default\n // gap round-trips the same way, so it must be excluded here rather than\n // just relying on absence — otherwise every imported paragraph would carry\n // an explicit (if harmless) `paragraphSpacing` equal to the default.\n const gap = readParagraphGap(attrs);\n const paragraphSpacing =\n gap !== null && gap !== RICH_TEXT_SPACING.paragraphGap ? gap : undefined;\n\n const block = createParagraphBlock({\n content: styleParagraphs(\n ensureParagraphWrapped(html),\n readParagraphStyle(attrs, ctx),\n ),\n ...(paragraphSpacing !== undefined ? { paragraphSpacing } : {}),\n ...baseFields(attrs),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * Resolve an `mj-text` to Title, Table, Menu or Paragraph by the shape of its\n * content — the reverse of the four renderers that all emit `mj-text`.\n *\n * A fifth renderer emits it too: `HtmlBlock` (`renderers/html.ts`), whose\n * content is arbitrary, so it has no shape to match and lands on the Paragraph\n * fallback. That is irreducible — nothing in the output marks a block's type —\n * and narrowing the Title/Table shapes to compensate would break the common\n * case to serve the rare one. See §10 of the design.\n *\n * Paragraph is the terminal arm and always reachable, so this is total.\n */\nexport function convertTextElement(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted {\n const html = $el.html() ?? \"\";\n const root = rootElements(html);\n\n if (root.count === 1 && HEADING_LEVELS[root.tag]) {\n return convertTitle(html, attrs, HEADING_LEVELS[root.tag], ctx.cascade);\n }\n\n if (root.count === 1 && root.tag === \"table\") {\n return convertTable(html, attrs, ctx.cascade);\n }\n\n if (looksLikeMenu(html)) {\n return convertMenu(html, attrs, ctx.cascade);\n }\n\n return convertParagraph(html, attrs, ctx);\n}\n","import type { Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport {\n canonicalFontStack,\n createButtonBlock,\n createDividerBlock,\n createHtmlBlock,\n createImageBlock,\n createSpacerBlock,\n} from \"@templatical/types\";\nimport type { Block, ButtonBlock, DividerBlock } from \"@templatical/types\";\nimport {\n parseAlignment,\n parseBorderStyle,\n parseColor,\n parsePaddingShorthand,\n parsePxValue,\n} from \"./attribute-parser\";\nimport {\n ownAttr,\n resolveAttributes,\n tagOf,\n type Attrs,\n} from \"./attribute-resolver\";\nimport {\n baseFields,\n convertHtmlFallback,\n isNewTab,\n readBorder,\n readBorderRadius,\n warnForeignClasses,\n type ConvertContext,\n type Converted,\n} from \"./block-base\";\nimport {\n convertNativeTable,\n convertNavbar,\n convertSocial,\n} from \"./composite-mapper\";\nimport { convertTextElement } from \"./text-inference\";\n\n/** Tags handled elsewhere but recognised, so they never hit the unknown-tag arm. */\nconst STRUCTURAL_TAGS = new Set([\n \"mjml\",\n \"mj-head\",\n \"mj-body\",\n \"mj-wrapper\",\n \"mj-section\",\n \"mj-column\",\n \"mj-group\",\n \"mj-attributes\",\n \"mj-all\",\n \"mj-class\",\n \"mj-font\",\n \"mj-style\",\n \"mj-title\",\n \"mj-preview\",\n \"mj-breakpoint\",\n \"mj-html-attributes\",\n \"mj-social-element\",\n \"mj-navbar-link\",\n]);\n\n/** Tags with no Templatical equivalent that keep their markup verbatim. */\nconst NO_EQUIVALENT_TAGS = new Set([\"mj-hero\", \"mj-carousel\", \"mj-accordion\"]);\n\nfunction convertImage(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted | null {\n const src = (attrs.src ?? \"\").trim();\n if (!src) return null;\n\n const decorative = (attrs.role ?? \"\").trim().toLowerCase() === \"presentation\";\n const pxWidth = parsePxValue(attrs.width);\n const height = parsePxValue(attrs.height);\n const href = (attrs.href ?? \"\").trim();\n\n const notes: string[] = [];\n const borderRadius = readBorderRadius(attrs, \"Image\", notes);\n const border = readBorder(attrs, \"Image\", notes);\n\n return {\n block: createImageBlock({\n src,\n alt: decorative ? \"\" : (attrs.alt ?? \"\"),\n // A px width equal to the container is how `width: \"full\"` renders\n // (renderer/src/renderers/image.ts), so restore the flag rather than\n // freezing the number — otherwise a full-width image stops resizing with\n // the template.\n width: pxWidth === ctx.containerWidth ? \"full\" : pxWidth || \"full\",\n align: parseAlignment(attrs.align, \"center\"),\n ...(height > 0 ? { height } : {}),\n ...(borderRadius ? { borderRadius } : {}),\n ...(border ? { border } : {}),\n ...(href ? { linkUrl: href } : {}),\n ...(href && isNewTab(attrs) ? { linkOpenInNewTab: true } : {}),\n ...(decorative ? { decorative: true } : {}),\n ...baseFields(attrs),\n }),\n entry: {\n sourceTag: \"mj-image\",\n templaticalBlockType: \"image\",\n status: notes.length > 0 ? \"approximated\" : \"converted\",\n ...(notes.length > 0 ? { note: notes.join(\" \") } : {}),\n },\n };\n}\n\n/**\n * `mj-button`'s `width` as `ButtonBlock.width`, where no width is a button\n * that fits its label. `100%` is `\"full\"` and a px width stays px, as the\n * renderer writes them. Any other percentage becomes px of `fillWidth`, the\n * box MJML sizes it against, since the block holds no percentage. A width\n * with no size fits the label, which is what MJML draws for it. Anything but\n * `100%` or a positive px width pushes a note.\n */\nfunction readButtonWidth(\n value: string | undefined,\n fillWidth: number,\n notes: string[],\n): ButtonBlock[\"width\"] {\n const raw = (value ?? \"\").trim();\n if (!raw) return undefined;\n\n const match = /^(-?\\d+(?:\\.\\d+)?)\\s*(px|%)?$/i.exec(raw);\n if (!match) {\n notes.push(\n `Button width \"${raw}\" could not be read; imported as fit to content.`,\n );\n return undefined;\n }\n const parsed = parseFloat(match[1]);\n const isPercent = match[2] === \"%\";\n\n if (isPercent && parsed >= 100) {\n if (parsed > 100) {\n notes.push(`Button width ${raw} was clamped to 100%.`);\n }\n return \"full\";\n }\n\n const px = Math.round(isPercent ? (parsed / 100) * fillWidth : parsed);\n if (px <= 0) {\n notes.push(\n `Button width ${raw} has no usable size; imported as fit to content.`,\n );\n return undefined;\n }\n if (isPercent) {\n notes.push(\n `Button width ${raw} was imported as ${px}px, which does not scale with its column.`,\n );\n }\n return px;\n}\n\n/**\n * A percentage `width` sizes the button against its column less the column's\n * side padding and its own, the same box an `mj-divider` line spans.\n *\n * `font-family` is read only when the button sets its own: the renderer\n * writes the document font onto `mj-all`, which reaches every button.\n */\nfunction convertButton(\n $el: Cheerio<Element>,\n attrs: Attrs,\n ctx: ConvertContext,\n): Converted | null {\n const text = ($el.text() ?? \"\").trim();\n if (!text) return null;\n\n const backgroundColor = parseColor(attrs[\"background-color\"]);\n const textColor = parseColor(attrs.color);\n const fontSize = parsePxValue(attrs[\"font-size\"]);\n const fontFamily = canonicalFontStack(\n ownAttr(attrs, \"font-family\", \"mj-button\", ctx.cascade),\n );\n const base = baseFields(attrs);\n const { left, right } = base.styles.padding;\n\n const notes: string[] = [];\n const width = readButtonWidth(\n attrs.width,\n ctx.containerWidth - ctx.columnPadding - left - right,\n notes,\n );\n const borderRadius = readBorderRadius(attrs, \"Button\", notes);\n const border = readBorder(attrs, \"Button\", notes);\n\n return {\n block: createButtonBlock({\n text,\n url: (attrs.href ?? \"\").trim(),\n ...(backgroundColor ? { backgroundColor } : {}),\n ...(textColor ? { textColor } : {}),\n ...(fontSize > 0 ? { fontSize } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...(borderRadius !== undefined ? { borderRadius } : {}),\n ...(border ? { border } : {}),\n ...(attrs[\"inner-padding\"] !== undefined\n ? { buttonPadding: parsePaddingShorthand(attrs[\"inner-padding\"]) }\n : {}),\n ...(width !== undefined ? { width } : {}),\n align: parseAlignment(attrs.align, \"center\"),\n ...(isNewTab(attrs) ? { openInNewTab: true } : {}),\n ...base,\n }),\n entry: {\n sourceTag: \"mj-button\",\n templaticalBlockType: \"button\",\n status: notes.length > 0 ? \"approximated\" : \"converted\",\n ...(notes.length > 0 ? { note: notes.join(\" \") } : {}),\n },\n };\n}\n\n/**\n * `mj-divider`'s `width` as `DividerBlock.width`. A percentage is a share of\n * the column and stays one, to two decimals within 0–100%. `100%`, and a px\n * width that reaches `lineWidth`, span the line's whole box, which is\n * `\"full\"`. A clamped or unreadable value pushes a note.\n *\n * It reads a sign, which `parsePercent` does not, so a negative value is\n * clamped to 0 and reported instead of being dropped.\n */\nfunction readDividerWidth(\n value: string | undefined,\n lineWidth: number,\n notes: string[],\n): DividerBlock[\"width\"] {\n const raw = (value ?? \"\").trim();\n if (!raw) return \"full\";\n\n const match = /^(-?\\d+(?:\\.\\d+)?)\\s*(px|%)?$/i.exec(raw);\n if (!match) {\n notes.push(\n `Divider width \"${raw}\" could not be read; imported as full width.`,\n );\n return \"full\";\n }\n const parsed = parseFloat(match[1]);\n\n if (match[2] === \"%\") {\n const percent = Math.min(100, Math.max(0, parsed));\n if (percent !== parsed) {\n notes.push(`Divider width ${raw} was clamped to ${percent}%.`);\n }\n // Two decimals keep the value inside `DividerPercentWidth`'s pattern,\n // which a float printed in exponent form would leave.\n const share = Math.round(percent * 100) / 100;\n return share === 100 ? \"full\" : `${share}%`;\n }\n\n const px = Math.max(0, Math.round(parsed));\n if (px !== Math.round(parsed)) {\n notes.push(`Divider width ${raw} was clamped to 0px.`);\n }\n return px >= lineWidth ? \"full\" : px;\n}\n\n/**\n * `mj-divider` draws `100%` across its column less the column's side padding\n * and its own, so that is the width a px value is measured against. It places\n * a partial-width line by `align`, centred by default; Templatical centres\n * every divider, so a left- or right-aligned partial one comes back with a\n * note.\n */\nfunction convertDivider(attrs: Attrs, ctx: ConvertContext): Converted {\n const color = parseColor(attrs[\"border-color\"]);\n const thickness = parsePxValue(attrs[\"border-width\"]);\n const base = baseFields(attrs);\n const { left, right } = base.styles.padding;\n\n const notes: string[] = [];\n const width = readDividerWidth(\n attrs.width,\n ctx.containerWidth - ctx.columnPadding - left - right,\n notes,\n );\n\n const align = (attrs.align ?? \"\").trim().toLowerCase();\n if (width !== \"full\" && (align === \"left\" || align === \"right\")) {\n notes.push(\n `MJML aligns this divider ${align}; Templatical centres every divider.`,\n );\n }\n\n return {\n block: createDividerBlock({\n lineStyle: parseBorderStyle(attrs[\"border-style\"]),\n width,\n ...(color ? { color } : {}),\n ...(attrs[\"border-width\"] !== undefined ? { thickness } : {}),\n ...base,\n }),\n entry: {\n sourceTag: \"mj-divider\",\n templaticalBlockType: \"divider\",\n status: notes.length > 0 ? \"approximated\" : \"converted\",\n ...(notes.length > 0 ? { note: notes.join(\" \") } : {}),\n },\n };\n}\n\nfunction convertSpacer(attrs: Attrs): Block {\n const height = parsePxValue(attrs.height);\n\n return createSpacerBlock({\n ...(attrs.height !== undefined ? { height } : {}),\n ...baseFields(attrs),\n });\n}\n\n/**\n * Convert one MJML element to a Templatical block.\n *\n * Returns `null` for an element that produces nothing *and* warrants no report\n * entry — an image with no `src`, a button with no label. A `Converted` whose\n * `block` is null is a *skip*, which does get an entry.\n */\nexport function convertElement(\n $el: Cheerio<Element>,\n ctx: ConvertContext,\n): Converted | null {\n const tag = tagOf($el[0]);\n if (!tag) return null;\n\n const attrs = resolveAttributes($el, ctx.cascade);\n warnForeignClasses(attrs, tag, ctx);\n\n if (tag === \"mj-include\") {\n const path = (attrs.path ?? \"\").trim();\n return {\n block: null,\n entry: {\n sourceTag: tag,\n templaticalBlockType: null,\n status: \"skipped\",\n note: `Cannot resolve <mj-include path=\"${path}\"> — the importer reads a single string and has no filesystem access. Inline the include before importing.`,\n },\n };\n }\n\n if (tag === \"mj-text\") {\n return convertTextElement($el, attrs, ctx);\n }\n\n if (tag === \"mj-social\") {\n return convertSocial($el, attrs, ctx);\n }\n\n if (tag === \"mj-navbar\") {\n return convertNavbar($el, attrs, ctx);\n }\n\n if (tag === \"mj-table\") {\n return convertNativeTable($el, attrs, ctx);\n }\n\n if (tag === \"mj-image\") {\n return convertImage($el, attrs, ctx);\n }\n\n if (tag === \"mj-button\") {\n return convertButton($el, attrs, ctx);\n }\n\n if (tag === \"mj-divider\") {\n return convertDivider(attrs, ctx);\n }\n\n if (tag === \"mj-spacer\") {\n return {\n block: convertSpacer(attrs),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"spacer\",\n status: \"converted\",\n },\n };\n }\n\n if (tag === \"mj-raw\") {\n return {\n block: createHtmlBlock({\n content: $el.html() ?? \"\",\n ...baseFields(attrs),\n }),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"converted\",\n },\n };\n }\n\n if (NO_EQUIVALENT_TAGS.has(tag)) {\n return {\n block: convertHtmlFallback($el, ctx, attrs),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `<${tag}> has no Templatical block equivalent; the original markup is preserved.`,\n },\n };\n }\n\n if (STRUCTURAL_TAGS.has(tag)) return null;\n\n return {\n block: convertHtmlFallback($el, ctx, attrs),\n entry: {\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `<${tag}> is not a known MJML element (a custom component?); the original markup is preserved.`,\n },\n };\n}\n","import type { Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport { SYNTAX_PRESETS } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport { tagOf } from \"./attribute-resolver\";\n\ntype DisplayCondition = NonNullable<Block[\"displayCondition\"]>;\n\nexport interface SiblingUnit {\n $el: Cheerio<Element>;\n displayCondition?: DisplayCondition;\n}\n\n/** Characters kept before an ellipsis is appended; the full condition stays in `before`. */\nconst LABEL_MAX = 46;\n\nconst ANCHORED_LOGIC = Object.values(SYNTAX_PRESETS).map(\n (preset) =>\n new RegExp(\n `^(?:${preset.logic.source})$`,\n preset.logic.flags.replace(\"g\", \"\"),\n ),\n);\n\n/**\n * Whether the text is exactly one logic tag and nothing else.\n *\n * Anchored against every registered syntax rather than one, so a mailchimp or\n * ampscript template is recognised as readily as a liquid one. MSO conditionals\n * are HTML comments and match none of them, which is what keeps hand-written\n * `mj-raw` pairs out of this path.\n */\nexport function isLogicTagOnly(text: string): boolean {\n const trimmed = text.trim();\n if (trimmed === \"\") return false;\n return ANCHORED_LOGIC.some((regex) => regex.test(trimmed));\n}\n\nfunction isLogicRaw($el: Cheerio<Element>): boolean {\n return tagOf($el[0]) === \"mj-raw\" && isLogicTagOnly($el.text() ?? \"\");\n}\n\nfunction synthesizeLabel(before: string): string {\n const trimmed = before.trim();\n if (trimmed.length <= LABEL_MAX) return trimmed;\n return `${trimmed.slice(0, LABEL_MAX)}…`;\n}\n\n/**\n * Group a run of siblings into units, folding each `logic-raw / element /\n * logic-raw` triple into one unit carrying a `displayCondition`.\n *\n * `label` is synthesised from `before`: it is editor metadata that appears\n * nowhere in the MJML, so it cannot be recovered — only reconstructed. `group`\n * and `description` are left absent for the same reason.\n *\n * Deliberately no check that the two tags pair *semantically*. The renderer\n * emits them only in this arrangement, and matching open/close keywords across\n * four syntaxes would be a table to maintain for no gain.\n */\nexport function planSiblings($siblings: Cheerio<Element>[]): SiblingUnit[] {\n const units: SiblingUnit[] = [];\n let i = 0;\n\n while (i < $siblings.length) {\n const $current = $siblings[i];\n\n const $middle = $siblings[i + 1];\n const $closing = $siblings[i + 2];\n\n const isTriple =\n isLogicRaw($current) &&\n $middle !== undefined &&\n tagOf($middle[0]) !== \"mj-raw\" &&\n $closing !== undefined &&\n isLogicRaw($closing);\n\n if (isTriple) {\n const before = ($current.text() ?? \"\").trim();\n const after = ($closing.text() ?? \"\").trim();\n units.push({\n $el: $middle,\n displayCondition: { label: synthesizeLabel(before), before, after },\n });\n i += 3;\n continue;\n }\n\n units.push({ $el: $current });\n i += 1;\n }\n\n return units;\n}\n","import type { CheerioAPI } from \"cheerio\";\nimport {\n canonicalFontStack,\n DEFAULT_TEMPLATE_DEFAULTS,\n resolveContentDirection,\n} from \"@templatical/types\";\nimport type {\n ContentDirection,\n TemplateContent,\n TemplateSettings,\n} from \"@templatical/types\";\nimport { parseColor, parsePxValue } from \"./attribute-parser\";\nimport {\n childElements,\n findByTag,\n tagOf,\n type AttributeCascade,\n} from \"./attribute-resolver\";\n\n/** `mj-head` children this module reads; anything else is warned about. */\nconst CONSUMED_HEAD_TAGS = new Set([\n \"mj-attributes\",\n \"mj-preview\",\n \"mj-font\",\n \"mj-style\",\n \"mj-title\",\n]);\n\n/**\n * `DEFAULT_TEMPLATE_DEFAULTS` is typed `Partial<TemplateSettings>` so a\n * consumer can override any subset of it, but its own literal (see\n * `packages/types/src/defaults.ts`) always sets exactly the six required\n * `TemplateSettings` fields — `linkColor` and `preheaderText` are the two\n * optional ones and are correctly absent from it. Narrowing the type once\n * here, rather than at every read below, is what lets `width`, `fontFamily`\n * and the rest come out as `number`/`string`/`boolean` instead of `| undefined`.\n */\nconst REQUIRED_TEMPLATE_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\ninterface AnchorRule {\n color?: string;\n underline?: boolean;\n}\n\n/**\n * Read the `a { … }` declarations out of the concatenated `mj-style` blocks.\n *\n * This is the reverse of how the renderer emits `settings.linkColor` and\n * `settings.linkUnderline` — as a global anchor rule — so a template that\n * round-trips keeps both. A stylesheet with no anchor rule leaves both unset.\n */\nfunction readAnchorRule(css: string): AnchorRule {\n const rule: AnchorRule = {};\n\n // Strip CSS comments to avoid breaking property matching when comments\n // precede or separate declarations (e.g. `a { /* note */ color: #0055ff; }`).\n const cssWithoutComments = css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n\n for (const match of cssWithoutComments.matchAll(\n /(^|[},])\\s*a\\s*\\{([^}]*)\\}/g,\n )) {\n const body = match[2];\n\n const color = body.match(/(?:^|;)\\s*color\\s*:\\s*([^;]+)/i);\n if (color) {\n const parsed = parseColor(color[1]);\n if (parsed) rule.color = parsed;\n }\n\n const decoration = body.match(/(?:^|;)\\s*text-decoration\\s*:\\s*([^;]+)/i);\n if (decoration) {\n rule.underline = decoration[1].trim().toLowerCase().includes(\"underline\");\n }\n }\n\n return rule;\n}\n\n/**\n * `<mjml dir>` as `settings.direction`, kept only when it disagrees with the\n * direction the locale implies. The renderer always writes `dir`, resolved\n * from the locale when a template states no direction, so keeping a value that\n * agrees would add a key the source template never set. `auto`, MJML's\n * default, states no direction.\n */\nfunction readDirection(\n value: string | undefined,\n locale: string,\n): ContentDirection | undefined {\n const dir = (value ?? \"\").trim().toLowerCase();\n if (dir !== \"ltr\" && dir !== \"rtl\") return undefined;\n return dir === resolveContentDirection({ locale }) ? undefined : dir;\n}\n\n/**\n * Build `TemplateSettings` from the `mjml` and `mj-body` attributes, the\n * attribute cascade and the remaining `mj-head` children.\n *\n * Optional keys (`preheaderText`, `linkColor`, `direction`) are **omitted**\n * rather than set to `undefined`: an absent key is what the block model means\n * by unset, and a present-but-undefined key serialises into exported JSON.\n */\nexport function extractSettings(\n $: CheerioAPI,\n cascade: AttributeCascade,\n warnings: string[],\n): TemplateContent[\"settings\"] {\n const $body = findByTag($, \"mj-body\").first();\n const $root = findByTag($, \"mjml\").first();\n\n const width =\n parsePxValue($body.attr(\"width\")) || REQUIRED_TEMPLATE_DEFAULTS.width;\n const backgroundColor =\n parseColor($body.attr(\"background-color\")) ||\n REQUIRED_TEMPLATE_DEFAULTS.backgroundColor;\n\n const fontFromCascade =\n canonicalFontStack(cascade.all[\"font-family\"]) ||\n canonicalFontStack(cascade.byTag[\"mj-text\"]?.[\"font-family\"]);\n const fontFromDeclaration =\n findByTag($, \"mj-font\").first().attr(\"name\") ?? \"\";\n const fontFamily =\n fontFromCascade ||\n fontFromDeclaration ||\n REQUIRED_TEMPLATE_DEFAULTS.fontFamily;\n\n const textColor =\n parseColor(cascade.byTag[\"mj-text\"]?.color) ||\n REQUIRED_TEMPLATE_DEFAULTS.textColor;\n\n const previewText = findByTag($, \"mj-preview\").first().text().trim();\n\n // Every <mj-style> block in the document is pooled into one stylesheet before\n // the anchor rule is read, so a bare `a { … }` rule in any of them contributes.\n // Against the renderer's own output that is unambiguous: every other rule it\n // emits is scoped under a class (`.tpl-rich-text-8 p`, `.tpl-button-full a`),\n // so the one bare `a { … }` rule present is the document-level rule this\n // function reads back.\n const styleCss = findByTag($, \"mj-style\")\n .toArray()\n .map((el) => $(el).text())\n .join(\"\\n\");\n const anchor = readAnchorRule(styleCss);\n\n const locale =\n ($root.attr(\"lang\") ?? \"\").trim() || REQUIRED_TEMPLATE_DEFAULTS.locale;\n const direction = readDirection($root.attr(\"dir\"), locale);\n\n const title = findByTag($, \"mj-title\").first().text().trim();\n if (title) {\n warnings.push(\n `Dropped <mj-title> (\"${title}\") — Templatical templates have no document-title field.`,\n );\n }\n\n const $head = findByTag($, \"mj-head\").first();\n if ($head.length > 0) {\n for (const $child of childElements($head, $)) {\n const tag = tagOf($child[0]);\n if (!CONSUMED_HEAD_TAGS.has(tag)) {\n warnings.push(`Dropped <${tag}> — it has no Templatical equivalent.`);\n }\n }\n }\n\n return {\n width,\n backgroundColor,\n textColor,\n linkUnderline: anchor.underline ?? REQUIRED_TEMPLATE_DEFAULTS.linkUnderline,\n fontFamily,\n locale,\n ...(direction ? { direction } : {}),\n ...(anchor.color ? { linkColor: anchor.color } : {}),\n ...(previewText ? { preheaderText: previewText } : {}),\n };\n}\n","import type { Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport { createSectionBlock } from \"@templatical/types\";\nimport type {\n Block,\n ColumnLayout,\n SectionWrapper,\n SpacingValue,\n} from \"@templatical/types\";\nimport {\n parseColor,\n parseDefinitePx,\n parsePaddingShorthand,\n parsePercent,\n} from \"./attribute-parser\";\nimport {\n childElements,\n resolveAttributes,\n tagOf,\n type Attrs,\n} from \"./attribute-resolver\";\nimport { convertElement } from \"./block-mapper\";\nimport {\n baseFields,\n convertHtmlFallback,\n readBorder,\n readBorderRadius,\n type ConvertContext,\n} from \"./block-base\";\nimport { planSiblings } from \"./display-condition\";\nimport type { ImportReportEntry } from \"./types\";\n\n/** Reverse of `packages/renderer/src/columns.ts`, keyed by column count. */\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\n/** Percentage points of drift tolerated per column before a match is inexact. */\nconst WIDTH_TOLERANCE = 2;\n\n/**\n * Resolve MJML column widths to one of the five layouts `ColumnLayout` allows.\n *\n * `exact: false` means the caller must report `approximated` — MJML permits any\n * number of columns at any width and this union permits five shapes, so this is\n * the importer's main irreducible loss (§8.1).\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 // No widths at all is MJML's \"distribute equally\", which is exactly what the\n // n-column 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 // A partial mix of known and absent widths fills each absent column with\n // its share of what the known widths leave over, not an equal split of the\n // whole row — an equal split is only correct when every column is absent,\n // which the branch above already handles. Clamped at 0 so a document whose\n // explicit widths already exceed 100% cannot produce a negative fill.\n const nullCount = percents.filter((p) => p === null).length;\n const knownSum = percents.reduce((sum: number, p) => sum + (p ?? 0), 0);\n const remainder = nullCount > 0 ? Math.max(0, 100 - knownSum) / nullCount : 0;\n const resolved = percents.map((p) => p ?? remainder);\n\n const sameCount = LAYOUT_SHAPES.filter(\n (shape) => shape.percents.length === count,\n );\n\n for (const shape of sameCount) {\n const fits = shape.percents.every(\n (want, i) => Math.abs(want - resolved[i]) <= WIDTH_TOLERANCE,\n );\n if (fits) return { layout: shape.layout, exact: true };\n }\n\n // Nearest same-count shape by total absolute error; if the count itself has\n // no shape (4+), collapse to \"3\" and let the caller fold the overflow.\n const candidates =\n sameCount.length > 0\n ? sameCount\n : LAYOUT_SHAPES.filter((shape) => shape.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\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\n/** Column pixel widths per layout, mirroring `renderer/src/columns.ts`. */\nfunction columnPixels(layout: ColumnLayout, containerWidth: number): number[] {\n switch (layout) {\n case \"2\":\n return [containerWidth * 0.5, containerWidth * 0.5];\n case \"3\":\n return [containerWidth / 3, containerWidth / 3, containerWidth / 3];\n case \"1-2\":\n return [containerWidth / 3, (containerWidth * 2) / 3];\n case \"2-1\":\n return [(containerWidth * 2) / 3, containerWidth / 3];\n default:\n return [containerWidth];\n }\n}\n\n/**\n * The `mj-column` elements of a section, in document order: a direct\n * `mj-column` child contributes itself, and an `mj-group` child contributes\n * every `mj-column` it holds at the group's own position — so a section\n * mixing direct columns with one or more groups keeps every column instead\n * of losing all but the first group's. `grouped` is true whenever any\n * `mj-group` child is present, which is what drives `stackOnMobile: false`.\n */\nfunction readColumns(\n $el: Cheerio<Element>,\n ctx: ConvertContext,\n): { columns: Cheerio<Element>[]; grouped: boolean } {\n const kids = childElements($el, ctx.$);\n const columns: Cheerio<Element>[] = [];\n let grouped = false;\n\n for (const $kid of kids) {\n const tag = tagOf($kid[0]);\n if (tag === \"mj-column\") {\n columns.push($kid);\n } else if (tag === \"mj-group\") {\n grouped = true;\n columns.push(\n ...childElements($kid, ctx.$).filter(\n ($k) => tagOf($k[0]) === \"mj-column\",\n ),\n );\n }\n }\n\n return { columns, grouped };\n}\n\n/**\n * A column's children, in document order, one block per unit `planSiblings`\n * groups them into.\n *\n * Routed through `planSiblings` for the same reason `walkBody` is\n * (`converter.ts`): the renderer wraps a conditional block in bracketing\n * `mj-raw` guards inside a column exactly as it does at top level\n * (`renderers/section.ts` calls the same `wrapWithDisplayCondition` helper as\n * `index.ts`), so recovering the condition here needs the identical fold.\n */\nfunction convertColumnChildren(\n $column: Cheerio<Element>,\n ctx: ConvertContext,\n entries: ImportReportEntry[],\n): Block[] {\n const blocks: Block[] = [];\n\n for (const unit of planSiblings(childElements($column, ctx.$))) {\n const tag = tagOf(unit.$el[0]);\n\n // MJML forbids mj-section inside mj-column, and `addBlock` in\n // @templatical/core refuses a section into a column too, so producing one\n // here would be dropped downstream without a trace.\n if (tag === \"mj-section\" || tag === \"mj-wrapper\") {\n const attrs = resolveAttributes(unit.$el, ctx.cascade);\n const fallback = convertHtmlFallback(unit.$el, ctx, attrs);\n if (unit.displayCondition)\n fallback.displayCondition = unit.displayCondition;\n blocks.push(fallback);\n const noun = tag === \"mj-section\" ? \"section\" : \"wrapper\";\n entries.push({\n sourceTag: tag,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `MJML forbids <${tag}> inside <mj-column>; the nested ${noun}'s markup is preserved as an html block.`,\n });\n continue;\n }\n\n const converted = convertElement(unit.$el, ctx);\n if (!converted) continue;\n entries.push(converted.entry);\n if (!converted.block) continue;\n\n if (unit.displayCondition) {\n converted.block.displayCondition = unit.displayCondition;\n }\n\n blocks.push(converted.block);\n }\n\n return blocks;\n}\n\n/**\n * A column's width as a percentage of the section's container, or `null`\n * when the column carries no definite width of its own.\n *\n * `mj-column` accepts either a percentage or a px length, and a px width is\n * real geometry — converting it here lets it participate in\n * `matchColumnLayout` as a known value instead of falling through to \"auto\"\n * and losing the author's intended ratio.\n */\nfunction columnWidthPercent(\n value: string | undefined,\n containerWidth: number,\n): number | null {\n const percent = parsePercent(value);\n if (percent !== null) return percent;\n\n const px = parseDefinitePx(value);\n return px !== null && containerWidth > 0 ? (px / containerWidth) * 100 : null;\n}\n\n/**\n * Build a `SectionBlock` (always exactly one) from an `mj-section`.\n *\n * Returns an array so the caller can treat sections and wrappers uniformly.\n */\nexport function buildSection(\n $el: Cheerio<Element>,\n ctx: ConvertContext,\n entries: ImportReportEntry[],\n wrapper?: SectionWrapper,\n): Block[] {\n const attrs = resolveAttributes($el, ctx.cascade);\n const { columns, grouped } = readColumns($el, ctx);\n\n const columnAttrs = columns.map(($c) => resolveAttributes($c, ctx.cascade));\n const rawWidths = columnAttrs.map((column) => column.width);\n const percents = rawWidths.map((width) =>\n columnWidthPercent(width, ctx.containerWidth),\n );\n const { layout, exact } = matchColumnLayout(percents);\n const slots = COLUMN_COUNT[layout];\n const pixels = columnPixels(layout, ctx.containerWidth);\n\n // `entries` is the document-wide report the whole walk shares, not a\n // per-section array, so the section's own entry is pushed here — before its\n // children convert — to land ahead of them and ahead of whatever a sibling\n // section pushes next. Splicing it in afterward (e.g. `unshift`) would put\n // every section's entry at the front of the entire report instead of at the\n // front of its own children, reversing section order in a multi-section\n // document and interleaving children under the wrong parent.\n //\n // Built from the raw attribute strings, not the resolved `percents` — a px\n // width would otherwise show as a repeating-decimal percentage of the\n // container instead of the value the author actually wrote.\n const shown = rawWidths.map((w) => w || \"auto\").join(\", \");\n const notes: string[] = exact\n ? []\n : [\n `Column widths ${shown} have no exact Templatical layout; resolved to \"${layout}\".`,\n ];\n const borderRadius = readBorderRadius(attrs, \"Section\", notes);\n const border = readBorder(attrs, \"Section\", notes);\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 const children: Block[][] = Array.from({ length: slots }, () => []);\n\n columns.forEach(($column, index) => {\n // A 4th+ column folds into the last slot rather than becoming an html\n // block: its content converts perfectly and only the geometry is lost.\n const slot = Math.min(index, slots - 1);\n const { left, right } = parsePaddingShorthand(columnAttrs[index].padding);\n const columnCtx: ConvertContext = {\n ...ctx,\n containerWidth: Math.round(pixels[slot] ?? ctx.containerWidth),\n columnPadding: left + right,\n };\n children[slot].push(...convertColumnChildren($column, columnCtx, entries));\n });\n\n const section = createSectionBlock({\n columns: layout,\n children,\n ...(grouped ? { stackOnMobile: false } : {}),\n ...(borderRadius ? { borderRadius } : {}),\n ...(border ? { border } : {}),\n ...(wrapper ? { wrapper } : {}),\n // \"native\": mj-section is the one element whose own background-color\n // attribute is its container fill (renderers/section.ts:32) — every other\n // baseFields caller reads container-background-color instead.\n ...baseFields(attrs, \"native\"),\n });\n\n return [section];\n}\n\nconst WRAPPER_BORDER_ATTRIBUTES = [\n \"border\",\n \"border-top\",\n \"border-right\",\n \"border-bottom\",\n \"border-left\",\n];\n\n/**\n * `SectionWrapper` has no border, so each `border` or `border-<side>` the\n * wrapper sets pushes a note instead.\n */\nfunction readWrapper(attrs: Attrs, notes: string[]): SectionWrapper {\n const backgroundColor = parseColor(attrs[\"background-color\"]);\n const padding: SpacingValue = parsePaddingShorthand(attrs.padding);\n const borderRadius = readBorderRadius(attrs, \"Wrapper\", notes);\n\n for (const name of WRAPPER_BORDER_ATTRIBUTES) {\n const raw = attrs[name];\n if (raw === undefined) continue;\n notes.push(\n `Wrapper ${name} \"${raw.trim()}\" has no Templatical equivalent; dropped.`,\n );\n }\n\n return {\n ...(backgroundColor ? { backgroundColor } : {}),\n padding,\n ...(borderRadius ? { borderRadius } : {}),\n };\n}\n\n/**\n * Fold an `mj-wrapper` into the `wrapper` field of the section(s) it holds.\n *\n * A wrapper is not a block: `SectionWrapper` is exactly the band the renderer\n * emits an `mj-wrapper` for (`renderer/src/index.ts:223`), so representing it\n * as its own section would double the nesting on every round trip.\n */\nexport function buildWrapper(\n $el: Cheerio<Element>,\n ctx: ConvertContext,\n entries: ImportReportEntry[],\n): Block[] {\n const attrs = resolveAttributes($el, ctx.cascade);\n const notes: string[] = [];\n const wrapper = readWrapper(attrs, notes);\n\n const sections = childElements($el, ctx.$).filter(\n ($k) => tagOf($k[0]) === \"mj-section\",\n );\n\n if (sections.length === 0) {\n entries.push({\n sourceTag: \"mj-wrapper\",\n templaticalBlockType: null,\n status: \"skipped\",\n note: \"An <mj-wrapper> with no <mj-section> children produces nothing.\",\n });\n return [];\n }\n\n if (sections.length > 1) {\n notes.unshift(\n `An <mj-wrapper> holding ${sections.length} sections was applied to each of them — Templatical has no multi-section band.`,\n );\n }\n\n // A wrapper is not a block of its own, so it gets an entry only when it has\n // something to report.\n if (notes.length > 0) {\n entries.push({\n sourceTag: \"mj-wrapper\",\n templaticalBlockType: \"section\",\n status: \"approximated\",\n note: notes.join(\" \"),\n });\n }\n\n return sections.flatMap(($section) =>\n buildSection($section, ctx, entries, wrapper),\n );\n}\n","import { load } from \"cheerio\";\nimport type { Cheerio } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport {\n createDefaultTemplateContent,\n createSectionBlock,\n resolveContentDirection,\n} from \"@templatical/types\";\nimport type { Block, TemplateContent } from \"@templatical/types\";\nimport {\n buildAttributeCascade,\n childElements,\n findByTag,\n tagOf,\n} from \"./attribute-resolver\";\nimport { convertElement } from \"./block-mapper\";\nimport type { ConvertContext } from \"./block-base\";\nimport { planSiblings } from \"./display-condition\";\nimport { extractSettings } from \"./head-parser\";\nimport { buildSection, buildWrapper } from \"./section-builder\";\nimport type { ImportReport, ImportReportEntry, ImportResult } from \"./types\";\n\nconst EMPTY_DOCUMENT_WARNING =\n \"No convertible content was found in the MJML. Check that the document has an <mj-body> with at least one <mj-section>.\";\n\n/**\n * Wrap blocks that sat directly under `mj-body` in a one-column section.\n *\n * Valid MJML puts every block inside an `mj-section`, but hand-written and\n * machine-mangled documents do not, and the editor canvas has no\n * representation for a block outside a section.\n */\nfunction wrapInSection(blocks: Block[]): Block {\n return createSectionBlock({\n columns: \"1\",\n children: [blocks],\n styles: { padding: { top: 0, right: 0, bottom: 0, left: 0 } },\n });\n}\n\nfunction walkBody(\n $body: Cheerio<Element>,\n ctx: ConvertContext,\n entries: ImportReportEntry[],\n): Block[] {\n const blocks: Block[] = [];\n let loose: Block[] = [];\n\n const flushLoose = () => {\n if (loose.length > 0) {\n blocks.push(wrapInSection(loose));\n loose = [];\n }\n };\n\n for (const unit of planSiblings(childElements($body, ctx.$))) {\n const tag = tagOf(unit.$el[0]);\n\n if (tag === \"mj-wrapper\" || tag === \"mj-section\") {\n flushLoose();\n const produced =\n tag === \"mj-wrapper\"\n ? buildWrapper(unit.$el, ctx, entries)\n : buildSection(unit.$el, ctx, entries);\n\n // A condition brackets one element, so it lands on every block that\n // element produced — which is more than one only for a multi-section\n // wrapper, where each band carries the same guard.\n for (const block of produced) {\n if (unit.displayCondition)\n block.displayCondition = unit.displayCondition;\n blocks.push(block);\n }\n continue;\n }\n\n const converted = convertElement(unit.$el, ctx);\n if (!converted) continue;\n entries.push(converted.entry);\n if (!converted.block) continue;\n\n if (unit.displayCondition) {\n converted.block.displayCondition = unit.displayCondition;\n }\n\n // An html fallback for a body-level element is already a top-level block;\n // wrapping it in a section would add nesting the source never had.\n if (converted.entry.status === \"html-fallback\") {\n flushLoose();\n blocks.push(converted.block);\n continue;\n }\n\n loose.push(converted.block);\n }\n\n flushLoose();\n return blocks;\n}\n\n/**\n * Convert an MJML document into a Templatical template.\n *\n * @example\n * ```ts\n * const { content, report } = convertMjmlTemplate(mjmlSource);\n *\n * const editor = init({ container: '#editor', content });\n *\n * console.log(report.summary);\n * console.log(report.warnings);\n * ```\n */\nexport function convertMjmlTemplate(mjml: string): ImportResult {\n if (typeof mjml !== \"string\") {\n throw new Error(\n \"Invalid MJML template: expected a string. Pass the raw MJML source as a string.\",\n );\n }\n if (mjml.trim().length === 0) {\n throw new Error(\n \"Invalid MJML template: input is empty. Pass the raw MJML source of an email.\",\n );\n }\n\n // The `xml` option routes parsing through htmlparser2 rather than parse5, so\n // custom `mj-*` tags survive as generic elements and no implicit\n // <html><head><body> is injected around them. Two htmlparser2 options are\n // then overridden away from what `xml: true` alone would give:\n //\n // - `xmlMode: false` puts htmlparser2 in HTML mode, which knows the void\n // element list (`br`, `img`, `hr`, …) and closes them immediately. In XML\n // mode a bare `<br>` stays open and everything after it — including the\n // rest of the paragraph — becomes ITS CHILD rather than its sibling. That\n // shape is exactly what TipTap emits for a hard break and what browser DOM\n // serialization produces, so it hits `<mj-text>` content routinely; a\n // later HTML-mode reparse (e.g. loading the block into the editor) then\n // invents a *second* `<br>` from the dangling `</br>`, per the HTML5 rule\n // that a stray `<br>` end tag opens a new `<br>` rather than closing one.\n // - `recognizeSelfClosing: true` restores what HTML mode otherwise gives up:\n // without it, a self-closing custom tag like `<mj-image src=\"…\" />` never\n // actually closes, and swallows whatever follows as its child instead of\n // its sibling — verified against a self-closing `<mj-all />` immediately\n // followed by a sibling `<mj-text />` inside `<mj-attributes>`, which\n // nested the second tag inside the first and silently dropped a per-tag\n // attribute default.\n const $ = load(mjml, { xml: { xmlMode: false, recognizeSelfClosing: true } });\n const cascade = buildAttributeCascade($);\n\n const entries: ImportReportEntry[] = [];\n const warnings: string[] = [];\n\n const settings = extractSettings($, cascade, warnings);\n\n const $body = findByTag($, \"mj-body\").first();\n const ctx: ConvertContext = {\n $,\n cascade,\n containerWidth: settings.width,\n columnPadding: 0,\n direction: resolveContentDirection(settings),\n warnings,\n };\n\n const blocks = $body.length > 0 ? walkBody($body, ctx, entries) : [];\n\n if (blocks.length === 0) {\n warnings.push(EMPTY_DOCUMENT_WARNING);\n }\n\n const content: TemplateContent = {\n ...createDefaultTemplateContent(),\n blocks,\n 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"],"mappings":"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"}
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@templatical/import-mjml",
3
3
  "description": "Convert MJML email templates to Templatical format",
4
- "version": "0.44.3",
4
+ "version": "0.46.0",
5
5
  "bugs": "https://github.com/templatical/sdk/issues",
6
6
  "dependencies": {
7
- "@templatical/types": "0.44.3",
7
+ "@templatical/types": "0.46.0",
8
8
  "cheerio": "^1.2.0",
9
9
  "domhandler": "^6.0.1"
10
10
  },
11
11
  "devDependencies": {
12
- "@templatical/renderer": "0.44.3",
12
+ "@templatical/renderer": "0.46.0",
13
13
  "@types/node": "^25.9.9",
14
14
  "typescript": "^6.0.3",
15
15
  "vitest": "^5.0.3"