@templatical/import-unlayer
Advanced tools
+125
-124
@@ -1,3 +0,2 @@ | ||
| import * as _templatical_types from '@templatical/types'; | ||
| //#region src/types.d.ts | ||
| /** | ||
@@ -9,145 +8,145 @@ * Unlayer JSON type definitions. | ||
| interface UnlayerTemplate { | ||
| counters?: Record<string, number>; | ||
| body: UnlayerBody; | ||
| schemaVersion?: number; | ||
| counters?: Record<string, number>; | ||
| body: UnlayerBody; | ||
| schemaVersion?: number; | ||
| } | ||
| interface UnlayerBody { | ||
| id?: string; | ||
| rows: UnlayerRow[]; | ||
| headers?: UnlayerRow[]; | ||
| footers?: UnlayerRow[]; | ||
| values: UnlayerBodyValues; | ||
| id?: string; | ||
| rows: UnlayerRow[]; | ||
| headers?: UnlayerRow[]; | ||
| footers?: UnlayerRow[]; | ||
| values: UnlayerBodyValues; | ||
| } | ||
| interface UnlayerBackgroundImage { | ||
| url?: string; | ||
| fullWidth?: boolean; | ||
| repeat?: string; | ||
| size?: string; | ||
| position?: string; | ||
| url?: string; | ||
| fullWidth?: boolean; | ||
| repeat?: string; | ||
| size?: string; | ||
| position?: string; | ||
| } | ||
| interface UnlayerFontFamily { | ||
| label?: string; | ||
| value?: string; | ||
| label?: string; | ||
| value?: string; | ||
| } | ||
| interface UnlayerBodyValues { | ||
| backgroundColor?: string; | ||
| backgroundImage?: UnlayerBackgroundImage; | ||
| contentWidth?: string; | ||
| contentAlignment?: string; | ||
| fontFamily?: UnlayerFontFamily; | ||
| textColor?: string; | ||
| linkStyle?: Record<string, string>; | ||
| preheaderText?: string; | ||
| [key: string]: unknown; | ||
| backgroundColor?: string; | ||
| backgroundImage?: UnlayerBackgroundImage; | ||
| contentWidth?: string; | ||
| contentAlignment?: string; | ||
| fontFamily?: UnlayerFontFamily; | ||
| textColor?: string; | ||
| linkStyle?: Record<string, string>; | ||
| preheaderText?: string; | ||
| [key: string]: unknown; | ||
| } | ||
| interface UnlayerRow { | ||
| id?: string; | ||
| cells: number[]; | ||
| columns: UnlayerColumn[]; | ||
| values: UnlayerRowValues; | ||
| id?: string; | ||
| cells: number[]; | ||
| columns: UnlayerColumn[]; | ||
| values: UnlayerRowValues; | ||
| } | ||
| interface UnlayerRowValues { | ||
| backgroundColor?: string; | ||
| backgroundImage?: UnlayerBackgroundImage; | ||
| padding?: string; | ||
| columnsBackgroundColor?: string; | ||
| hideMobile?: boolean; | ||
| hideDesktop?: boolean; | ||
| noStackMobile?: boolean; | ||
| _meta?: Record<string, unknown>; | ||
| [key: string]: unknown; | ||
| backgroundColor?: string; | ||
| backgroundImage?: UnlayerBackgroundImage; | ||
| padding?: string; | ||
| columnsBackgroundColor?: string; | ||
| hideMobile?: boolean; | ||
| hideDesktop?: boolean; | ||
| noStackMobile?: boolean; | ||
| _meta?: Record<string, unknown>; | ||
| [key: string]: unknown; | ||
| } | ||
| interface UnlayerColumn { | ||
| id?: string; | ||
| contents: UnlayerContent[]; | ||
| values: UnlayerColumnValues; | ||
| id?: string; | ||
| contents: UnlayerContent[]; | ||
| values: UnlayerColumnValues; | ||
| } | ||
| interface UnlayerColumnValues { | ||
| backgroundColor?: string; | ||
| padding?: string; | ||
| border?: Record<string, string>; | ||
| _meta?: Record<string, unknown>; | ||
| [key: string]: unknown; | ||
| backgroundColor?: string; | ||
| padding?: string; | ||
| border?: Record<string, string>; | ||
| _meta?: Record<string, unknown>; | ||
| [key: string]: unknown; | ||
| } | ||
| interface UnlayerContent { | ||
| type: string; | ||
| values: UnlayerContentValues; | ||
| type: string; | ||
| values: UnlayerContentValues; | ||
| } | ||
| interface UnlayerLinkAction { | ||
| name?: string; | ||
| values?: { | ||
| href?: string; | ||
| target?: string; | ||
| }; | ||
| name?: string; | ||
| values?: { | ||
| href?: string; | ||
| target?: string; | ||
| }; | ||
| } | ||
| interface UnlayerImageSrc { | ||
| url?: string; | ||
| width?: number; | ||
| height?: number; | ||
| url?: string; | ||
| width?: number; | ||
| height?: number; | ||
| } | ||
| interface UnlayerButtonColors { | ||
| color?: string; | ||
| backgroundColor?: string; | ||
| hoverColor?: string; | ||
| hoverBackgroundColor?: string; | ||
| color?: string; | ||
| backgroundColor?: string; | ||
| hoverColor?: string; | ||
| hoverBackgroundColor?: string; | ||
| } | ||
| interface UnlayerBorder { | ||
| borderTopWidth?: string; | ||
| borderTopStyle?: string; | ||
| borderTopColor?: string; | ||
| borderTopWidth?: string; | ||
| borderTopStyle?: string; | ||
| borderTopColor?: string; | ||
| } | ||
| interface UnlayerMenuItem { | ||
| key?: string; | ||
| text: string; | ||
| link?: UnlayerLinkAction; | ||
| key?: string; | ||
| text: string; | ||
| link?: UnlayerLinkAction; | ||
| } | ||
| interface UnlayerMenu { | ||
| items: UnlayerMenuItem[]; | ||
| items: UnlayerMenuItem[]; | ||
| } | ||
| interface UnlayerSocialIcon { | ||
| name?: string; | ||
| url?: string; | ||
| type?: string; | ||
| name?: string; | ||
| url?: string; | ||
| type?: string; | ||
| } | ||
| interface UnlayerSocialIcons { | ||
| iconType?: string; | ||
| editor?: { | ||
| data?: { | ||
| showTitle?: boolean; | ||
| }; | ||
| iconType?: string; | ||
| editor?: { | ||
| data?: { | ||
| showTitle?: boolean; | ||
| }; | ||
| icons?: UnlayerSocialIcon[]; | ||
| }; | ||
| icons?: UnlayerSocialIcon[]; | ||
| } | ||
| interface UnlayerContentValues { | ||
| containerPadding?: string; | ||
| textAlign?: string; | ||
| fontFamily?: UnlayerFontFamily; | ||
| fontSize?: string; | ||
| fontWeight?: string | number; | ||
| color?: string; | ||
| hideMobile?: boolean; | ||
| hideDesktop?: boolean; | ||
| text?: string; | ||
| headingType?: string; | ||
| src?: UnlayerImageSrc; | ||
| altText?: string; | ||
| action?: UnlayerLinkAction; | ||
| buttonColors?: UnlayerButtonColors; | ||
| borderRadius?: string; | ||
| padding?: string; | ||
| size?: { | ||
| autoWidth?: boolean; | ||
| width?: string; | ||
| }; | ||
| href?: UnlayerLinkAction; | ||
| border?: UnlayerBorder; | ||
| containerPadding?: string; | ||
| textAlign?: string; | ||
| fontFamily?: UnlayerFontFamily; | ||
| fontSize?: string; | ||
| fontWeight?: string | number; | ||
| color?: string; | ||
| hideMobile?: boolean; | ||
| hideDesktop?: boolean; | ||
| text?: string; | ||
| headingType?: string; | ||
| src?: UnlayerImageSrc; | ||
| altText?: string; | ||
| action?: UnlayerLinkAction; | ||
| buttonColors?: UnlayerButtonColors; | ||
| borderRadius?: string; | ||
| padding?: string; | ||
| size?: { | ||
| autoWidth?: boolean; | ||
| width?: string; | ||
| html?: string; | ||
| menu?: UnlayerMenu; | ||
| layout?: "horizontal" | "vertical"; | ||
| separator?: string; | ||
| icons?: UnlayerSocialIcons; | ||
| videoUrl?: string; | ||
| thumbnailUrl?: string; | ||
| [key: string]: unknown; | ||
| }; | ||
| href?: UnlayerLinkAction; | ||
| border?: UnlayerBorder; | ||
| width?: string; | ||
| html?: string; | ||
| menu?: UnlayerMenu; | ||
| layout?: "horizontal" | "vertical"; | ||
| separator?: string; | ||
| icons?: UnlayerSocialIcons; | ||
| videoUrl?: string; | ||
| thumbnailUrl?: string; | ||
| [key: string]: unknown; | ||
| } | ||
@@ -162,6 +161,6 @@ /** | ||
| interface ImportReportEntry { | ||
| unlayerContentType: string; | ||
| templaticalBlockType: string | null; | ||
| status: ConversionStatus; | ||
| note?: string; | ||
| unlayerContentType: string; | ||
| templaticalBlockType: string | null; | ||
| status: ConversionStatus; | ||
| note?: string; | ||
| } | ||
@@ -172,11 +171,11 @@ /** | ||
| interface ImportReport { | ||
| entries: ImportReportEntry[]; | ||
| warnings: string[]; | ||
| summary: { | ||
| total: number; | ||
| converted: number; | ||
| approximated: number; | ||
| htmlFallback: number; | ||
| skipped: number; | ||
| }; | ||
| entries: ImportReportEntry[]; | ||
| warnings: string[]; | ||
| summary: { | ||
| total: number; | ||
| converted: number; | ||
| approximated: number; | ||
| htmlFallback: number; | ||
| skipped: number; | ||
| }; | ||
| } | ||
@@ -187,6 +186,7 @@ /** | ||
| interface ImportResult { | ||
| content: _templatical_types.TemplateContent; | ||
| report: ImportReport; | ||
| content: import("@templatical/types").TemplateContent; | ||
| report: ImportReport; | ||
| } | ||
| //#endregion | ||
| //#region src/converter.d.ts | ||
| /** | ||
@@ -212,3 +212,4 @@ * Converts an Unlayer design JSON to Templatical TemplateContent. | ||
| declare function convertUnlayerTemplate(template: UnlayerTemplate): ImportResult; | ||
| //#endregion | ||
| export { type ConversionStatus, type ImportReport, type ImportReportEntry, type ImportResult, type UnlayerBody, type UnlayerColumn, type UnlayerContent, type UnlayerContentValues, type UnlayerRow, type UnlayerTemplate, convertUnlayerTemplate }; | ||
| //# sourceMappingURL=index.d.ts.map |
+533
-554
@@ -1,618 +0,597 @@ | ||
| // src/converter.ts | ||
| import { | ||
| createSectionBlock, | ||
| createDefaultTemplateContent | ||
| } from "@templatical/types"; | ||
| // src/block-mapper.ts | ||
| import { | ||
| createTitleBlock, | ||
| createParagraphBlock, | ||
| createImageBlock, | ||
| createButtonBlock, | ||
| createDividerBlock, | ||
| createSpacerBlock, | ||
| createHtmlBlock, | ||
| createSocialIconsBlock, | ||
| createMenuBlock, | ||
| createVideoBlock, | ||
| generateId | ||
| } from "@templatical/types"; | ||
| // src/style-parser.ts | ||
| import { createButtonBlock, createDefaultTemplateContent, createDividerBlock, createHtmlBlock, createImageBlock, createMenuBlock, createParagraphBlock, createSectionBlock, createSocialIconsBlock, createSpacerBlock, createTitleBlock, createVideoBlock, generateId } from "@templatical/types"; | ||
| //#region src/style-parser.ts | ||
| /** | ||
| * Parses CSS-like style values from Unlayer content values. | ||
| */ | ||
| function parsePxValue(value) { | ||
| if (value === void 0 || value === null) return 0; | ||
| if (typeof value === "number") return Math.round(value); | ||
| const match = value.match(/^(-?\d+(?:\.\d+)?)\s*(?:px\s*)?$/); | ||
| return match ? Math.round(parseFloat(match[1])) : 0; | ||
| if (value === void 0 || value === null) return 0; | ||
| if (typeof value === "number") return Math.round(value); | ||
| const match = value.match(/^(-?\d+(?:\.\d+)?)\s*(?:px\s*)?$/); | ||
| return match ? Math.round(parseFloat(match[1])) : 0; | ||
| } | ||
| function parseColor(value) { | ||
| if (!value || value === "transparent") return ""; | ||
| const trimmed = value.trim(); | ||
| if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed.toLowerCase(); | ||
| if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) { | ||
| const r = trimmed[1]; | ||
| const g = trimmed[2]; | ||
| const b = trimmed[3]; | ||
| return `#${r}${r}${g}${g}${b}${b}`.toLowerCase(); | ||
| } | ||
| return trimmed; | ||
| if (!value || value === "transparent") return ""; | ||
| const trimmed = value.trim(); | ||
| if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed.toLowerCase(); | ||
| if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) { | ||
| const r = trimmed[1]; | ||
| const g = trimmed[2]; | ||
| const b = trimmed[3]; | ||
| return `#${r}${r}${g}${g}${b}${b}`.toLowerCase(); | ||
| } | ||
| return trimmed; | ||
| } | ||
| function parsePaddingShorthand(value) { | ||
| if (!value) return { top: 0, right: 0, bottom: 0, left: 0 }; | ||
| const parts = value.trim().split(/\s+/); | ||
| const values = parts.map((p) => parsePxValue(p)); | ||
| switch (values.length) { | ||
| case 1: | ||
| return { | ||
| top: values[0], | ||
| right: values[0], | ||
| bottom: values[0], | ||
| left: values[0] | ||
| }; | ||
| case 2: | ||
| return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[0], | ||
| left: values[1] | ||
| }; | ||
| case 3: | ||
| return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[2], | ||
| left: values[1] | ||
| }; | ||
| default: | ||
| return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[2], | ||
| left: values[3] | ||
| }; | ||
| } | ||
| if (!value) return { | ||
| top: 0, | ||
| right: 0, | ||
| bottom: 0, | ||
| left: 0 | ||
| }; | ||
| const values = value.trim().split(/\s+/).map((p) => parsePxValue(p)); | ||
| switch (values.length) { | ||
| case 1: return { | ||
| top: values[0], | ||
| right: values[0], | ||
| bottom: values[0], | ||
| left: values[0] | ||
| }; | ||
| case 2: return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[0], | ||
| left: values[1] | ||
| }; | ||
| case 3: return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[2], | ||
| left: values[1] | ||
| }; | ||
| default: return { | ||
| top: values[0], | ||
| right: values[1], | ||
| bottom: values[2], | ||
| left: values[3] | ||
| }; | ||
| } | ||
| } | ||
| function parseBorderObject(border) { | ||
| if (!border) return { width: 0, style: "solid", color: "#000000" }; | ||
| return { | ||
| width: parsePxValue(border.borderTopWidth), | ||
| style: border.borderTopStyle || "solid", | ||
| color: parseColor(border.borderTopColor) || "#000000" | ||
| }; | ||
| if (!border) return { | ||
| width: 0, | ||
| style: "solid", | ||
| color: "#000000" | ||
| }; | ||
| return { | ||
| width: parsePxValue(border.borderTopWidth), | ||
| style: border.borderTopStyle || "solid", | ||
| color: parseColor(border.borderTopColor) || "#000000" | ||
| }; | ||
| } | ||
| function parseWidthPercent(value) { | ||
| if (!value) return 100; | ||
| const match = value.match(/^(\d+(?:\.\d+)?)\s*%/); | ||
| if (match) return Math.round(parseFloat(match[1])); | ||
| return 100; | ||
| if (!value) return 100; | ||
| const match = value.match(/^(\d+(?:\.\d+)?)\s*%/); | ||
| if (match) return Math.round(parseFloat(match[1])); | ||
| return 100; | ||
| } | ||
| function parseFontFamily(value) { | ||
| if (!value) return ""; | ||
| if (typeof value === "string") { | ||
| return value.split(",")[0].trim().replace(/['"]/g, ""); | ||
| } | ||
| if (value.value) return value.value.split(",")[0].trim().replace(/['"]/g, ""); | ||
| if (value.label) return value.label; | ||
| return ""; | ||
| if (!value) return ""; | ||
| if (typeof value === "string") return value.split(",")[0].trim().replace(/['"]/g, ""); | ||
| if (value.value) return value.value.split(",")[0].trim().replace(/['"]/g, ""); | ||
| if (value.label) return value.label; | ||
| return ""; | ||
| } | ||
| // src/block-mapper.ts | ||
| var SOCIAL_PLATFORM_MAP = { | ||
| facebook: "facebook", | ||
| twitter: "twitter", | ||
| x: "twitter", | ||
| instagram: "instagram", | ||
| linkedin: "linkedin", | ||
| youtube: "youtube", | ||
| tiktok: "tiktok", | ||
| pinterest: "pinterest", | ||
| email: "email", | ||
| mail: "email", | ||
| whatsapp: "whatsapp", | ||
| telegram: "telegram", | ||
| discord: "discord", | ||
| snapchat: "snapchat", | ||
| reddit: "reddit", | ||
| github: "github", | ||
| dribbble: "dribbble", | ||
| behance: "behance" | ||
| //#endregion | ||
| //#region src/block-mapper.ts | ||
| const SOCIAL_PLATFORM_MAP = { | ||
| facebook: "facebook", | ||
| twitter: "twitter", | ||
| x: "twitter", | ||
| instagram: "instagram", | ||
| linkedin: "linkedin", | ||
| youtube: "youtube", | ||
| tiktok: "tiktok", | ||
| pinterest: "pinterest", | ||
| email: "email", | ||
| mail: "email", | ||
| whatsapp: "whatsapp", | ||
| telegram: "telegram", | ||
| discord: "discord", | ||
| snapchat: "snapchat", | ||
| reddit: "reddit", | ||
| github: "github", | ||
| dribbble: "dribbble", | ||
| behance: "behance" | ||
| }; | ||
| function toAlign(value, fallback = "left") { | ||
| if (value === "left" || value === "center" || value === "right") return value; | ||
| return fallback; | ||
| if (value === "left" || value === "center" || value === "right") return value; | ||
| return fallback; | ||
| } | ||
| /** | ||
| * Strip every `<…>` from a button label and reject any stray `<` or `>` | ||
| * left behind (e.g. a truncated `<script`). The original | ||
| * `value.replace(/<[^>]*>/g, "")` was both polynomial-ReDoS over | ||
| * `<<<<…` inputs and an incomplete sanitizer — an unterminated `<script` | ||
| * would survive the strip. Downstream HTML-escapes the label at render | ||
| * time, but stripping here keeps the imported JSON clean. | ||
| */ | ||
| function stripTagsPlain(text) { | ||
| let out = ""; | ||
| let i = 0; | ||
| while (i < text.length) { | ||
| if (text[i] === "<") { | ||
| const close = text.indexOf(">", i + 1); | ||
| if (close === -1) { | ||
| break; | ||
| } | ||
| i = close + 1; | ||
| continue; | ||
| } | ||
| if (text[i] === ">") { | ||
| i++; | ||
| continue; | ||
| } | ||
| out += text[i]; | ||
| i++; | ||
| } | ||
| return out; | ||
| let out = ""; | ||
| let i = 0; | ||
| while (i < text.length) { | ||
| if (text[i] === "<") { | ||
| const close = text.indexOf(">", i + 1); | ||
| if (close === -1) break; | ||
| i = close + 1; | ||
| continue; | ||
| } | ||
| if (text[i] === ">") { | ||
| i++; | ||
| continue; | ||
| } | ||
| out += text[i]; | ||
| i++; | ||
| } | ||
| return out; | ||
| } | ||
| function toLineStyle(value, fallback = "solid") { | ||
| if (value === "solid" || value === "dashed" || value === "dotted") | ||
| return value; | ||
| return fallback; | ||
| if (value === "solid" || value === "dashed" || value === "dotted") return value; | ||
| return fallback; | ||
| } | ||
| function makeStyles(values) { | ||
| const padding = parsePaddingShorthand(values.containerPadding); | ||
| return { | ||
| padding | ||
| }; | ||
| return { padding: parsePaddingShorthand(values.containerPadding) }; | ||
| } | ||
| /** | ||
| * Apply Unlayer text-level styles as TipTap-compatible inline markup. | ||
| * Mirrors the BeeFree importer's helper but reads from Unlayer's flat | ||
| * values shape rather than a CSS style record. | ||
| */ | ||
| function inlineStylesToHtml(html, values) { | ||
| const spanParts = []; | ||
| const fontSize = parsePxValue(values.fontSize); | ||
| if (fontSize && fontSize !== 16) spanParts.push(`font-size: ${fontSize}px`); | ||
| const color = parseColor(values.color); | ||
| if (color && color !== "#1a1a1a") spanParts.push(`color: ${color}`); | ||
| const fontWeight = values.fontWeight; | ||
| if (fontWeight !== void 0 && fontWeight !== null && String(fontWeight) !== "normal" && String(fontWeight) !== "400") { | ||
| spanParts.push(`font-weight: ${fontWeight}`); | ||
| } | ||
| const fontFamily = parseFontFamily(values.fontFamily); | ||
| if (fontFamily) spanParts.push(`font-family: ${fontFamily}`); | ||
| const textAlign = values.textAlign; | ||
| const pStyle = textAlign && textAlign !== "left" ? `text-align: ${textAlign}` : ""; | ||
| if (!pStyle && spanParts.length === 0) return html; | ||
| const spanStyle = spanParts.join("; "); | ||
| let result = html; | ||
| if (pStyle) { | ||
| result = result.replace(/<p style="([^"]*)">/g, `<p style="$1; ${pStyle}">`).replaceAll("<p>", `<p style="${pStyle}">`); | ||
| } | ||
| if (spanStyle) { | ||
| result = wrapParagraphInner(result, spanStyle); | ||
| } | ||
| return result; | ||
| const spanParts = []; | ||
| const fontSize = parsePxValue(values.fontSize); | ||
| if (fontSize && fontSize !== 16) spanParts.push(`font-size: ${fontSize}px`); | ||
| const color = parseColor(values.color); | ||
| if (color && color !== "#1a1a1a") spanParts.push(`color: ${color}`); | ||
| const fontWeight = values.fontWeight; | ||
| if (fontWeight !== void 0 && fontWeight !== null && String(fontWeight) !== "normal" && String(fontWeight) !== "400") spanParts.push(`font-weight: ${fontWeight}`); | ||
| const fontFamily = parseFontFamily(values.fontFamily); | ||
| if (fontFamily) spanParts.push(`font-family: ${fontFamily}`); | ||
| const textAlign = values.textAlign; | ||
| const pStyle = textAlign && textAlign !== "left" ? `text-align: ${textAlign}` : ""; | ||
| if (!pStyle && spanParts.length === 0) return html; | ||
| const spanStyle = spanParts.join("; "); | ||
| let result = html; | ||
| if (pStyle) result = result.replace(/<p style="([^"]*)">/g, `<p style="$1; ${pStyle}">`).replaceAll("<p>", `<p style="${pStyle}">`); | ||
| if (spanStyle) result = wrapParagraphInner(result, spanStyle); | ||
| return result; | ||
| } | ||
| /** | ||
| * Wrap the inner content of every `<p …>…</p>` with `<span style="…">…</span>`. | ||
| * | ||
| * Hand-rolled linear scanner instead of `/<p([^>]*)>([\s\S]*?)<\/p>/g` because | ||
| * the regex is polynomial-ReDoS: the engine retries `[\s\S]*?<\/p>` at every | ||
| * `<p` start, so inputs like `<p>a<p>a<p>a…` (no `</p>` ever) cost O(n²). | ||
| * The scanner advances `i` monotonically via `indexOf`, keeping the work | ||
| * strictly O(n). | ||
| */ | ||
| function wrapParagraphInner(html, spanStyle) { | ||
| let out = ""; | ||
| let i = 0; | ||
| while (i < html.length) { | ||
| const open = html.indexOf("<p", i); | ||
| if (open === -1) { | ||
| out += html.substring(i); | ||
| break; | ||
| } | ||
| const afterTagName = html[open + 2]; | ||
| if (afterTagName !== ">" && afterTagName !== " " && afterTagName !== " " && afterTagName !== "\n" && afterTagName !== "\r" && afterTagName !== "/") { | ||
| out += html.substring(i, open + 2); | ||
| i = open + 2; | ||
| continue; | ||
| } | ||
| const openEnd = html.indexOf(">", open + 2); | ||
| if (openEnd === -1) { | ||
| out += html.substring(i); | ||
| break; | ||
| } | ||
| const closeStart = html.indexOf("</p>", openEnd + 1); | ||
| if (closeStart === -1) { | ||
| out += html.substring(i); | ||
| break; | ||
| } | ||
| const inner = html.substring(openEnd + 1, closeStart); | ||
| out += html.substring(i, openEnd + 1); | ||
| out += `<span style="${spanStyle}">${inner}</span></p>`; | ||
| i = closeStart + 4; | ||
| } | ||
| return out; | ||
| let out = ""; | ||
| let i = 0; | ||
| while (i < html.length) { | ||
| const open = html.indexOf("<p", i); | ||
| if (open === -1) { | ||
| out += html.substring(i); | ||
| break; | ||
| } | ||
| const afterTagName = html[open + 2]; | ||
| if (afterTagName !== ">" && afterTagName !== " " && afterTagName !== " " && afterTagName !== "\n" && afterTagName !== "\r" && afterTagName !== "/") { | ||
| out += html.substring(i, open + 2); | ||
| i = open + 2; | ||
| continue; | ||
| } | ||
| const openEnd = html.indexOf(">", open + 2); | ||
| if (openEnd === -1) { | ||
| out += html.substring(i); | ||
| break; | ||
| } | ||
| const closeStart = html.indexOf("</p>", openEnd + 1); | ||
| if (closeStart === -1) { | ||
| out += html.substring(i); | ||
| break; | ||
| } | ||
| const inner = html.substring(openEnd + 1, closeStart); | ||
| out += html.substring(i, openEnd + 1); | ||
| out += `<span style="${spanStyle}">${inner}</span></p>`; | ||
| i = closeStart + 4; | ||
| } | ||
| return out; | ||
| } | ||
| function ensureParagraphWrapped(html) { | ||
| if (!html) return "<p></p>"; | ||
| if (/<p[\s>]/i.test(html)) return html; | ||
| return `<p>${html}</p>`; | ||
| if (!html) return "<p></p>"; | ||
| if (/<p[\s>]/i.test(html)) return html; | ||
| return `<p>${html}</p>`; | ||
| } | ||
| function convertText(values) { | ||
| const html = ensureParagraphWrapped(values.text ?? ""); | ||
| return createParagraphBlock({ | ||
| content: inlineStylesToHtml(html, values), | ||
| styles: makeStyles(values) | ||
| }); | ||
| return createParagraphBlock({ | ||
| content: inlineStylesToHtml(ensureParagraphWrapped(values.text ?? ""), values), | ||
| styles: makeStyles(values) | ||
| }); | ||
| } | ||
| function parseHeadingLevel(tag) { | ||
| if (!tag) return 2; | ||
| const match = tag.match(/^h(\d)$/i); | ||
| if (match) { | ||
| const num = Number(match[1]); | ||
| if (num >= 1 && num <= 4) return num; | ||
| } | ||
| return 2; | ||
| if (!tag) return 2; | ||
| const match = tag.match(/^h(\d)$/i); | ||
| if (match) { | ||
| const num = Number(match[1]); | ||
| if (num >= 1 && num <= 4) return num; | ||
| } | ||
| return 2; | ||
| } | ||
| function convertHeading(values) { | ||
| const text = values.text ?? ""; | ||
| const stripped = text.replace(/^<h\d[^>]*>|<\/h\d>$/gi, ""); | ||
| const content = stripped ? `<p>${stripped}</p>` : "<p></p>"; | ||
| return createTitleBlock({ | ||
| content, | ||
| level: parseHeadingLevel(values.headingType), | ||
| color: parseColor(values.color) || "#1a1a1a", | ||
| textAlign: toAlign(values.textAlign), | ||
| fontFamily: parseFontFamily(values.fontFamily) || void 0, | ||
| styles: makeStyles(values) | ||
| }); | ||
| const stripped = (values.text ?? "").replace(/^<h\d[^>]*>|<\/h\d>$/gi, ""); | ||
| return createTitleBlock({ | ||
| content: stripped ? `<p>${stripped}</p>` : "<p></p>", | ||
| level: parseHeadingLevel(values.headingType), | ||
| color: parseColor(values.color) || "#1a1a1a", | ||
| textAlign: toAlign(values.textAlign), | ||
| fontFamily: parseFontFamily(values.fontFamily) || void 0, | ||
| styles: makeStyles(values) | ||
| }); | ||
| } | ||
| function convertImage(values) { | ||
| const src = values.src; | ||
| const action = values.action?.values; | ||
| return createImageBlock({ | ||
| src: src?.url || "", | ||
| alt: values.altText || "", | ||
| width: src?.width ? Math.round(src.width) : 600, | ||
| align: toAlign(values.textAlign, "center"), | ||
| linkUrl: action?.href || void 0, | ||
| linkOpenInNewTab: action?.target === "_blank" || void 0, | ||
| styles: makeStyles(values) | ||
| }); | ||
| const src = values.src; | ||
| const action = values.action?.values; | ||
| return createImageBlock({ | ||
| src: src?.url || "", | ||
| alt: values.altText || "", | ||
| width: src?.width ? Math.round(src.width) : 600, | ||
| align: toAlign(values.textAlign, "center"), | ||
| linkUrl: action?.href || void 0, | ||
| linkOpenInNewTab: action?.target === "_blank" || void 0, | ||
| styles: makeStyles(values) | ||
| }); | ||
| } | ||
| function convertButton(values) { | ||
| const colors = values.buttonColors ?? {}; | ||
| const padding = values.padding ? parsePaddingShorthand(values.padding) : { top: 12, right: 24, bottom: 12, left: 24 }; | ||
| const label = stripTagsPlain(values.text ?? "Button"); | ||
| const linkValues = values.href?.values; | ||
| return createButtonBlock({ | ||
| text: label, | ||
| url: linkValues?.href || "#", | ||
| openInNewTab: linkValues?.target === "_blank" || void 0, | ||
| backgroundColor: parseColor(colors.backgroundColor) || "#4f46e5", | ||
| textColor: parseColor(colors.color) || "#ffffff", | ||
| borderRadius: parsePxValue(values.borderRadius), | ||
| fontSize: parsePxValue(values.fontSize) || 16, | ||
| fontFamily: parseFontFamily(values.fontFamily) || void 0, | ||
| buttonPadding: padding, | ||
| styles: makeStyles(values) | ||
| }); | ||
| const colors = values.buttonColors ?? {}; | ||
| const padding = values.padding ? parsePaddingShorthand(values.padding) : { | ||
| top: 12, | ||
| right: 24, | ||
| bottom: 12, | ||
| left: 24 | ||
| }; | ||
| const label = stripTagsPlain(values.text ?? "Button"); | ||
| const linkValues = values.href?.values; | ||
| return createButtonBlock({ | ||
| text: label, | ||
| url: linkValues?.href || "#", | ||
| openInNewTab: linkValues?.target === "_blank" || void 0, | ||
| backgroundColor: parseColor(colors.backgroundColor) || "#4f46e5", | ||
| textColor: parseColor(colors.color) || "#ffffff", | ||
| borderRadius: parsePxValue(values.borderRadius), | ||
| fontSize: parsePxValue(values.fontSize) || 16, | ||
| fontFamily: parseFontFamily(values.fontFamily) || void 0, | ||
| buttonPadding: padding, | ||
| styles: makeStyles(values) | ||
| }); | ||
| } | ||
| function convertDivider(values) { | ||
| const border = parseBorderObject(values.border); | ||
| return createDividerBlock({ | ||
| lineStyle: toLineStyle(border.style), | ||
| color: border.color, | ||
| thickness: border.width || 1, | ||
| width: parseWidthPercent(values.width), | ||
| styles: makeStyles(values) | ||
| }); | ||
| const border = parseBorderObject(values.border); | ||
| return createDividerBlock({ | ||
| lineStyle: toLineStyle(border.style), | ||
| color: border.color, | ||
| thickness: border.width || 1, | ||
| width: parseWidthPercent(values.width), | ||
| styles: makeStyles(values) | ||
| }); | ||
| } | ||
| function convertSpacer(values) { | ||
| const padding = parsePaddingShorthand(values.containerPadding); | ||
| const height = parsePxValue(values.height) || padding.top + padding.bottom || 24; | ||
| return createSpacerBlock({ | ||
| height, | ||
| styles: makeStyles(values) | ||
| }); | ||
| const padding = parsePaddingShorthand(values.containerPadding); | ||
| return createSpacerBlock({ | ||
| height: parsePxValue(values.height) || padding.top + padding.bottom || 24, | ||
| styles: makeStyles(values) | ||
| }); | ||
| } | ||
| function convertHtml(values) { | ||
| return createHtmlBlock({ | ||
| content: values.html ?? "", | ||
| styles: makeStyles(values) | ||
| }); | ||
| return createHtmlBlock({ | ||
| content: values.html ?? "", | ||
| styles: makeStyles(values) | ||
| }); | ||
| } | ||
| function convertSocial(values, warnings) { | ||
| const iconList = values.icons?.icons ?? []; | ||
| const icons = []; | ||
| for (const unlayerIcon of iconList) { | ||
| const id = (unlayerIcon.name ?? "").toLowerCase(); | ||
| const platform = SOCIAL_PLATFORM_MAP[id]; | ||
| if (!platform) { | ||
| warnings.push( | ||
| `Unrecognized social icon "${unlayerIcon.name || id}" was skipped.` | ||
| ); | ||
| continue; | ||
| } | ||
| icons.push({ | ||
| id: generateId(), | ||
| platform, | ||
| url: unlayerIcon.url || "#" | ||
| }); | ||
| } | ||
| return createSocialIconsBlock({ | ||
| icons, | ||
| align: toAlign(values.textAlign, "center"), | ||
| styles: makeStyles(values) | ||
| }); | ||
| const iconList = values.icons?.icons ?? []; | ||
| const icons = []; | ||
| for (const unlayerIcon of iconList) { | ||
| const id = (unlayerIcon.name ?? "").toLowerCase(); | ||
| const platform = SOCIAL_PLATFORM_MAP[id]; | ||
| if (!platform) { | ||
| warnings.push(`Unrecognized social icon "${unlayerIcon.name || id}" was skipped.`); | ||
| continue; | ||
| } | ||
| icons.push({ | ||
| id: generateId(), | ||
| platform, | ||
| url: unlayerIcon.url || "#" | ||
| }); | ||
| } | ||
| return createSocialIconsBlock({ | ||
| icons, | ||
| align: toAlign(values.textAlign, "center"), | ||
| styles: makeStyles(values) | ||
| }); | ||
| } | ||
| function convertVideo(values) { | ||
| return createVideoBlock({ | ||
| url: values.videoUrl || "", | ||
| thumbnailUrl: values.thumbnailUrl || "", | ||
| alt: values.altText || "", | ||
| width: 600, | ||
| align: toAlign(values.textAlign, "center"), | ||
| styles: makeStyles(values) | ||
| }); | ||
| return createVideoBlock({ | ||
| url: values.videoUrl || "", | ||
| thumbnailUrl: values.thumbnailUrl || "", | ||
| alt: values.altText || "", | ||
| width: 600, | ||
| align: toAlign(values.textAlign, "center"), | ||
| styles: makeStyles(values) | ||
| }); | ||
| } | ||
| function convertMenu(values) { | ||
| const menu = values.menu; | ||
| const items = (menu?.items ?? []).map((item) => ({ | ||
| id: generateId(), | ||
| text: item.text || "", | ||
| url: item.link?.values?.href || "#", | ||
| openInNewTab: item.link?.values?.target === "_blank", | ||
| bold: false, | ||
| underline: false | ||
| })); | ||
| return createMenuBlock({ | ||
| items, | ||
| separator: values.separator || "|", | ||
| separatorColor: "#999999", | ||
| fontSize: parsePxValue(values.fontSize) || 14, | ||
| color: parseColor(values.color) || "#1a1a1a", | ||
| fontFamily: parseFontFamily(values.fontFamily) || void 0, | ||
| textAlign: toAlign(values.textAlign, "center"), | ||
| styles: makeStyles(values) | ||
| }); | ||
| return createMenuBlock({ | ||
| items: (values.menu?.items ?? []).map((item) => ({ | ||
| id: generateId(), | ||
| text: item.text || "", | ||
| url: item.link?.values?.href || "#", | ||
| openInNewTab: item.link?.values?.target === "_blank", | ||
| bold: false, | ||
| underline: false | ||
| })), | ||
| separator: values.separator || "|", | ||
| separatorColor: "#999999", | ||
| fontSize: parsePxValue(values.fontSize) || 14, | ||
| color: parseColor(values.color) || "#1a1a1a", | ||
| fontFamily: parseFontFamily(values.fontFamily) || void 0, | ||
| textAlign: toAlign(values.textAlign, "center"), | ||
| styles: makeStyles(values) | ||
| }); | ||
| } | ||
| function convertHtmlFallback(content, comment) { | ||
| const safe = comment.replace(/</g, "<").replace(/>/g, ">"); | ||
| return createHtmlBlock({ | ||
| content: `<div style="padding:12px;border:1px dashed #d1d5db;border-radius:6px;background:#fafafa;color:#6b7280;font-family:sans-serif;font-size:13px;">${safe}</div>`, | ||
| styles: makeStyles(content.values) | ||
| }); | ||
| return createHtmlBlock({ | ||
| content: `<div style="padding:12px;border:1px dashed #d1d5db;border-radius:6px;background:#fafafa;color:#6b7280;font-family:sans-serif;font-size:13px;">${comment.replace(/</g, "<").replace(/>/g, ">")}</div>`, | ||
| styles: makeStyles(content.values) | ||
| }); | ||
| } | ||
| /** | ||
| * Converts a single Unlayer content node to a Templatical block. | ||
| * Returns the block and a report entry. | ||
| */ | ||
| function convertContent(content, warnings) { | ||
| const type = content.type; | ||
| const values = content.values ?? {}; | ||
| switch (type) { | ||
| case "text": | ||
| return { | ||
| block: convertText(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "paragraph", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "heading": | ||
| return { | ||
| block: convertHeading(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "title", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "image": | ||
| return { | ||
| block: convertImage(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "image", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "button": | ||
| return { | ||
| block: convertButton(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "button", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "divider": | ||
| return { | ||
| block: convertDivider(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "divider", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "spacer": | ||
| return { | ||
| block: convertSpacer(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "spacer", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "html": | ||
| return { | ||
| block: convertHtml(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "html", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "menu": | ||
| return { | ||
| block: convertMenu(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "menu", | ||
| status: "approximated", | ||
| note: "Menu styles map approximately; review spacing and separator color." | ||
| } | ||
| }; | ||
| case "social": | ||
| return { | ||
| block: convertSocial(values, warnings), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "social", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "video": | ||
| return { | ||
| block: convertVideo(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "video", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "timer": | ||
| return { | ||
| block: convertHtmlFallback( | ||
| content, | ||
| "Unlayer timer block: rebuild manually in Templatical" | ||
| ), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "html", | ||
| status: "html-fallback", | ||
| note: "Timer modules have no direct Templatical equivalent; placeholder HTML inserted." | ||
| } | ||
| }; | ||
| case "form": | ||
| return { | ||
| block: convertHtmlFallback( | ||
| content, | ||
| "Unlayer form block: not supported in Templatical (most email clients block form submission)" | ||
| ), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: null, | ||
| status: "skipped", | ||
| note: "Unlayer forms have no Templatical equivalent and are skipped. Most email clients block form submission anyway." | ||
| } | ||
| }; | ||
| default: | ||
| return { | ||
| block: convertHtmlFallback( | ||
| content, | ||
| `Unsupported Unlayer content type: ${type}` | ||
| ), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "html", | ||
| status: "html-fallback", | ||
| note: `Unknown content type "${type}" converted to HTML block.` | ||
| } | ||
| }; | ||
| } | ||
| const type = content.type; | ||
| const values = content.values ?? {}; | ||
| switch (type) { | ||
| case "text": return { | ||
| block: convertText(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "paragraph", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "heading": return { | ||
| block: convertHeading(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "title", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "image": return { | ||
| block: convertImage(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "image", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "button": return { | ||
| block: convertButton(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "button", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "divider": return { | ||
| block: convertDivider(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "divider", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "spacer": return { | ||
| block: convertSpacer(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "spacer", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "html": return { | ||
| block: convertHtml(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "html", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "menu": return { | ||
| block: convertMenu(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "menu", | ||
| status: "approximated", | ||
| note: "Menu styles map approximately; review spacing and separator color." | ||
| } | ||
| }; | ||
| case "social": return { | ||
| block: convertSocial(values, warnings), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "social", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "video": return { | ||
| block: convertVideo(values), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "video", | ||
| status: "converted" | ||
| } | ||
| }; | ||
| case "timer": return { | ||
| block: convertHtmlFallback(content, "Unlayer timer block: rebuild manually in Templatical"), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "html", | ||
| status: "html-fallback", | ||
| note: "Timer modules have no direct Templatical equivalent; placeholder HTML inserted." | ||
| } | ||
| }; | ||
| case "form": return { | ||
| block: convertHtmlFallback(content, "Unlayer form block: not supported in Templatical (most email clients block form submission)"), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: null, | ||
| status: "skipped", | ||
| note: "Unlayer forms have no Templatical equivalent and are skipped. Most email clients block form submission anyway." | ||
| } | ||
| }; | ||
| default: return { | ||
| block: convertHtmlFallback(content, `Unsupported Unlayer content type: ${type}`), | ||
| entry: { | ||
| unlayerContentType: type, | ||
| templaticalBlockType: "html", | ||
| status: "html-fallback", | ||
| note: `Unknown content type "${type}" converted to HTML block.` | ||
| } | ||
| }; | ||
| } | ||
| } | ||
| // src/converter.ts | ||
| //#endregion | ||
| //#region src/converter.ts | ||
| function resolveColumnLayout(cells, warnings) { | ||
| if (cells.length <= 1) return "1"; | ||
| if (cells.length === 3) return "3"; | ||
| if (cells.length === 2) { | ||
| const left = cells[0] ?? 1; | ||
| const right = cells[1] ?? 1; | ||
| const total = left + right; | ||
| const ratio = left / total; | ||
| if (ratio > 0.58) return "2-1"; | ||
| if (ratio < 0.42) return "1-2"; | ||
| return "2"; | ||
| } | ||
| warnings.push( | ||
| `Row with ${cells.length} columns was flattened to a single column. Unlayer supports arbitrary columns, but Templatical supports up to 3.` | ||
| ); | ||
| return "1"; | ||
| if (cells.length <= 1) return "1"; | ||
| if (cells.length === 3) return "3"; | ||
| if (cells.length === 2) { | ||
| const left = cells[0] ?? 1; | ||
| const ratio = left / (left + (cells[1] ?? 1)); | ||
| if (ratio > .58) return "2-1"; | ||
| if (ratio < .42) return "1-2"; | ||
| return "2"; | ||
| } | ||
| warnings.push(`Row with ${cells.length} columns was flattened to a single column. Unlayer supports arbitrary columns, but Templatical supports up to 3.`); | ||
| return "1"; | ||
| } | ||
| /** | ||
| * Converts all contents in a column to Templatical blocks. | ||
| */ | ||
| function convertColumnContents(column, entries, warnings) { | ||
| const blocks = []; | ||
| for (const content of column.contents ?? []) { | ||
| const { block, entry } = convertContent(content, warnings); | ||
| blocks.push(block); | ||
| entries.push(entry); | ||
| } | ||
| return blocks; | ||
| const blocks = []; | ||
| for (const content of column.contents ?? []) { | ||
| const { block, entry } = convertContent(content, warnings); | ||
| blocks.push(block); | ||
| entries.push(entry); | ||
| } | ||
| return blocks; | ||
| } | ||
| /** | ||
| * Processes a single Unlayer row into one or more Templatical blocks. | ||
| */ | ||
| function processRow(row, entries, warnings) { | ||
| const columns = row.columns; | ||
| if (!columns || columns.length === 0) return []; | ||
| const cells = row.cells ?? columns.map(() => 1); | ||
| const layout = resolveColumnLayout(cells, warnings); | ||
| let children; | ||
| if (layout === "1") { | ||
| const merged = []; | ||
| for (const column of columns) { | ||
| merged.push(...convertColumnContents(column, entries, warnings)); | ||
| } | ||
| children = [merged]; | ||
| } else { | ||
| children = columns.map( | ||
| (col) => convertColumnContents(col, entries, warnings) | ||
| ); | ||
| } | ||
| const rowBg = parseColor(row.values?.backgroundColor); | ||
| const padding = parsePaddingShorthand(row.values?.padding); | ||
| const section = createSectionBlock({ | ||
| columns: layout, | ||
| children, | ||
| styles: { | ||
| padding, | ||
| ...rowBg ? { backgroundColor: rowBg } : {} | ||
| } | ||
| }); | ||
| return [section]; | ||
| const columns = row.columns; | ||
| if (!columns || columns.length === 0) return []; | ||
| const layout = resolveColumnLayout(row.cells ?? columns.map(() => 1), warnings); | ||
| let children; | ||
| if (layout === "1") { | ||
| const merged = []; | ||
| for (const column of columns) merged.push(...convertColumnContents(column, entries, warnings)); | ||
| children = [merged]; | ||
| } else children = columns.map((col) => convertColumnContents(col, entries, warnings)); | ||
| const rowBg = parseColor(row.values?.backgroundColor); | ||
| const padding = parsePaddingShorthand(row.values?.padding); | ||
| return [createSectionBlock({ | ||
| columns: layout, | ||
| children, | ||
| styles: { | ||
| padding, | ||
| ...rowBg ? { backgroundColor: rowBg } : {} | ||
| } | ||
| })]; | ||
| } | ||
| /** | ||
| * Extracts template-level settings from the Unlayer body values. | ||
| */ | ||
| function extractSettings(template) { | ||
| const values = template.body?.values ?? {}; | ||
| const width = parsePxValue(values.contentWidth); | ||
| const bgColor = parseColor(values.backgroundColor) || "#ffffff"; | ||
| const fontFamily = parseFontFamily(values.fontFamily) || "Arial"; | ||
| return { | ||
| width: width || 600, | ||
| backgroundColor: bgColor, | ||
| fontFamily, | ||
| locale: "en" | ||
| }; | ||
| const values = template.body?.values ?? {}; | ||
| const width = parsePxValue(values.contentWidth); | ||
| const bgColor = parseColor(values.backgroundColor) || "#ffffff"; | ||
| const fontFamily = parseFontFamily(values.fontFamily) || "Arial"; | ||
| return { | ||
| width: width || 600, | ||
| backgroundColor: bgColor, | ||
| fontFamily, | ||
| locale: "en" | ||
| }; | ||
| } | ||
| /** | ||
| * Converts an Unlayer design JSON to Templatical TemplateContent. | ||
| * | ||
| * @param template - The parsed Unlayer JSON object (the result of `editor.saveDesign(...)`) | ||
| * @returns An ImportResult with the converted content and a detailed report | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { convertUnlayerTemplate } from '@templatical/import-unlayer'; | ||
| * | ||
| * const unlayerJson = JSON.parse(fileContent); | ||
| * const { content, report } = convertUnlayerTemplate(unlayerJson); | ||
| * | ||
| * const editor = init({ container: '#editor', content }); | ||
| * | ||
| * console.log(report.summary); | ||
| * console.log(report.warnings); | ||
| * ``` | ||
| */ | ||
| function convertUnlayerTemplate(template) { | ||
| if (!template?.body?.rows) { | ||
| throw new Error( | ||
| "Invalid Unlayer template: missing body.rows. Ensure you are passing a valid Unlayer JSON design (the output of editor.saveDesign)." | ||
| ); | ||
| } | ||
| const entries = []; | ||
| const warnings = []; | ||
| const blocks = []; | ||
| const headers = template.body.headers ?? []; | ||
| const footers = template.body.footers ?? []; | ||
| if (headers.length > 0) { | ||
| warnings.push( | ||
| `${headers.length} Unlayer header row(s) were imported as regular rows at the top of the template.` | ||
| ); | ||
| for (const row of headers) { | ||
| blocks.push(...processRow(row, entries, warnings)); | ||
| } | ||
| } | ||
| for (const row of template.body.rows) { | ||
| blocks.push(...processRow(row, entries, warnings)); | ||
| } | ||
| if (footers.length > 0) { | ||
| warnings.push( | ||
| `${footers.length} Unlayer footer row(s) were imported as regular rows at the bottom of the template.` | ||
| ); | ||
| for (const row of footers) { | ||
| blocks.push(...processRow(row, entries, warnings)); | ||
| } | ||
| } | ||
| const content = { | ||
| ...createDefaultTemplateContent(), | ||
| blocks, | ||
| settings: extractSettings(template) | ||
| }; | ||
| const summary = { | ||
| total: entries.length, | ||
| converted: entries.filter((e) => e.status === "converted").length, | ||
| approximated: entries.filter((e) => e.status === "approximated").length, | ||
| htmlFallback: entries.filter((e) => e.status === "html-fallback").length, | ||
| skipped: entries.filter((e) => e.status === "skipped").length | ||
| }; | ||
| const report = { entries, warnings, summary }; | ||
| return { content, report }; | ||
| if (!template?.body?.rows) throw new Error("Invalid Unlayer template: missing body.rows. Ensure you are passing a valid Unlayer JSON design (the output of editor.saveDesign)."); | ||
| const entries = []; | ||
| const warnings = []; | ||
| const blocks = []; | ||
| const headers = template.body.headers ?? []; | ||
| const footers = template.body.footers ?? []; | ||
| if (headers.length > 0) { | ||
| warnings.push(`${headers.length} Unlayer header row(s) were imported as regular rows at the top of the template.`); | ||
| for (const row of headers) blocks.push(...processRow(row, entries, warnings)); | ||
| } | ||
| for (const row of template.body.rows) blocks.push(...processRow(row, entries, warnings)); | ||
| if (footers.length > 0) { | ||
| warnings.push(`${footers.length} Unlayer footer row(s) were imported as regular rows at the bottom of the template.`); | ||
| for (const row of footers) blocks.push(...processRow(row, entries, warnings)); | ||
| } | ||
| return { | ||
| content: { | ||
| ...createDefaultTemplateContent(), | ||
| blocks, | ||
| settings: extractSettings(template) | ||
| }, | ||
| report: { | ||
| entries, | ||
| warnings, | ||
| summary: { | ||
| total: entries.length, | ||
| converted: entries.filter((e) => e.status === "converted").length, | ||
| approximated: entries.filter((e) => e.status === "approximated").length, | ||
| htmlFallback: entries.filter((e) => e.status === "html-fallback").length, | ||
| skipped: entries.filter((e) => e.status === "skipped").length | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| export { | ||
| convertUnlayerTemplate | ||
| }; | ||
| //#endregion | ||
| export { convertUnlayerTemplate }; | ||
| //# sourceMappingURL=index.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../src/converter.ts","../src/block-mapper.ts","../src/style-parser.ts"],"sourcesContent":["import {\n createSectionBlock,\n createDefaultTemplateContent,\n} from \"@templatical/types\";\nimport type { Block, ColumnLayout, TemplateContent } from \"@templatical/types\";\nimport type {\n UnlayerTemplate,\n UnlayerRow,\n UnlayerColumn,\n ImportResult,\n ImportReport,\n ImportReportEntry,\n} from \"./types\";\nimport { convertContent } from \"./block-mapper\";\nimport {\n parsePxValue,\n parseColor,\n parseFontFamily,\n parsePaddingShorthand,\n} from \"./style-parser\";\n\nfunction resolveColumnLayout(\n cells: number[],\n warnings: string[],\n): ColumnLayout {\n if (cells.length <= 1) return \"1\";\n if (cells.length === 3) return \"3\";\n\n if (cells.length === 2) {\n const left = cells[0] ?? 1;\n const right = cells[1] ?? 1;\n const total = left + right;\n const ratio = left / total;\n\n if (ratio > 0.58) return \"2-1\";\n if (ratio < 0.42) return \"1-2\";\n return \"2\";\n }\n\n warnings.push(\n `Row with ${cells.length} columns was flattened to a single column. Unlayer supports arbitrary columns, but Templatical supports up to 3.`,\n );\n return \"1\";\n}\n\n/**\n * Converts all contents in a column to Templatical blocks.\n */\nfunction convertColumnContents(\n column: UnlayerColumn,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n const blocks: Block[] = [];\n\n for (const content of column.contents ?? []) {\n const { block, entry } = convertContent(content, warnings);\n blocks.push(block);\n entries.push(entry);\n }\n\n return blocks;\n}\n\n/**\n * Processes a single Unlayer row into one or more Templatical blocks.\n */\nfunction processRow(\n row: UnlayerRow,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n const columns = row.columns;\n if (!columns || columns.length === 0) return [];\n\n const cells = row.cells ?? columns.map(() => 1);\n const layout = resolveColumnLayout(cells, warnings);\n\n let children: Block[][];\n if (layout === \"1\") {\n const merged: Block[] = [];\n for (const column of columns) {\n merged.push(...convertColumnContents(column, entries, warnings));\n }\n children = [merged];\n } else {\n children = columns.map((col) =>\n convertColumnContents(col, entries, warnings),\n );\n }\n\n const rowBg = parseColor(row.values?.backgroundColor);\n const padding = parsePaddingShorthand(row.values?.padding);\n\n const section = createSectionBlock({\n columns: layout,\n children,\n styles: {\n padding,\n ...(rowBg ? { backgroundColor: rowBg } : {}),\n },\n });\n\n return [section];\n}\n\n/**\n * Extracts template-level settings from the Unlayer body values.\n */\nfunction extractSettings(\n template: UnlayerTemplate,\n): TemplateContent[\"settings\"] {\n const values = template.body?.values ?? {};\n\n const width = parsePxValue(values.contentWidth);\n const bgColor = parseColor(values.backgroundColor) || \"#ffffff\";\n const fontFamily = parseFontFamily(values.fontFamily) || \"Arial\";\n\n return {\n width: width || 600,\n backgroundColor: bgColor,\n fontFamily,\n locale: \"en\",\n };\n}\n\n/**\n * Converts an Unlayer design JSON to Templatical TemplateContent.\n *\n * @param template - The parsed Unlayer JSON object (the result of `editor.saveDesign(...)`)\n * @returns An ImportResult with the converted content and a detailed report\n *\n * @example\n * ```ts\n * import { convertUnlayerTemplate } from '@templatical/import-unlayer';\n *\n * const unlayerJson = JSON.parse(fileContent);\n * const { content, report } = convertUnlayerTemplate(unlayerJson);\n *\n * const editor = init({ container: '#editor', content });\n *\n * console.log(report.summary);\n * console.log(report.warnings);\n * ```\n */\nexport function convertUnlayerTemplate(\n template: UnlayerTemplate,\n): ImportResult {\n if (!template?.body?.rows) {\n throw new Error(\n \"Invalid Unlayer template: missing body.rows. Ensure you are passing a valid Unlayer JSON design (the output of editor.saveDesign).\",\n );\n }\n\n const entries: ImportReportEntry[] = [];\n const warnings: string[] = [];\n const blocks: Block[] = [];\n\n const headers = template.body.headers ?? [];\n const footers = template.body.footers ?? [];\n\n if (headers.length > 0) {\n warnings.push(\n `${headers.length} Unlayer header row(s) were imported as regular rows at the top of the template.`,\n );\n for (const row of headers) {\n blocks.push(...processRow(row, entries, warnings));\n }\n }\n\n for (const row of template.body.rows) {\n blocks.push(...processRow(row, entries, warnings));\n }\n\n if (footers.length > 0) {\n warnings.push(\n `${footers.length} Unlayer footer row(s) were imported as regular rows at the bottom of the template.`,\n );\n for (const row of footers) {\n blocks.push(...processRow(row, entries, warnings));\n }\n }\n\n const content: TemplateContent = {\n ...createDefaultTemplateContent(),\n blocks,\n settings: extractSettings(template),\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","import {\n createTitleBlock,\n createParagraphBlock,\n createImageBlock,\n createButtonBlock,\n createDividerBlock,\n createSpacerBlock,\n createHtmlBlock,\n createSocialIconsBlock,\n createMenuBlock,\n createVideoBlock,\n generateId,\n} from \"@templatical/types\";\nimport type {\n Block,\n HeadingLevel,\n SocialPlatform,\n SocialIcon,\n MenuItemData,\n} from \"@templatical/types\";\nimport type {\n UnlayerContent,\n UnlayerContentValues,\n ImportReportEntry,\n} from \"./types\";\nimport {\n parsePxValue,\n parseColor,\n parseFontFamily,\n parsePaddingShorthand,\n parseBorderObject,\n parseWidthPercent,\n} from \"./style-parser\";\n\nconst SOCIAL_PLATFORM_MAP: Record<string, SocialPlatform> = {\n facebook: \"facebook\",\n twitter: \"twitter\",\n x: \"twitter\",\n instagram: \"instagram\",\n linkedin: \"linkedin\",\n youtube: \"youtube\",\n tiktok: \"tiktok\",\n pinterest: \"pinterest\",\n email: \"email\",\n mail: \"email\",\n whatsapp: \"whatsapp\",\n telegram: \"telegram\",\n discord: \"discord\",\n snapchat: \"snapchat\",\n reddit: \"reddit\",\n github: \"github\",\n dribbble: \"dribbble\",\n behance: \"behance\",\n};\n\ntype Align = \"left\" | \"center\" | \"right\";\ntype LineStyle = \"solid\" | \"dashed\" | \"dotted\";\n\nfunction toAlign(value: string | undefined, fallback: Align = \"left\"): Align {\n if (value === \"left\" || value === \"center\" || value === \"right\") return value;\n return fallback;\n}\n\n/**\n * Strip every `<…>` from a button label and reject any stray `<` or `>`\n * left behind (e.g. a truncated `<script`). The original\n * `value.replace(/<[^>]*>/g, \"\")` was both polynomial-ReDoS over\n * `<<<<…` inputs and an incomplete sanitizer — an unterminated `<script`\n * would survive the strip. Downstream HTML-escapes the label at render\n * time, but stripping here keeps the imported JSON clean.\n */\nfunction stripTagsPlain(text: string): string {\n let out = \"\";\n let i = 0;\n while (i < text.length) {\n if (text[i] === \"<\") {\n const close = text.indexOf(\">\", i + 1);\n if (close === -1) {\n // Unterminated tag — discard the rest.\n break;\n }\n i = close + 1;\n continue;\n }\n if (text[i] === \">\") {\n i++;\n continue;\n }\n out += text[i];\n i++;\n }\n return out;\n}\n\nfunction toLineStyle(\n value: string | undefined,\n fallback: LineStyle = \"solid\",\n): LineStyle {\n if (value === \"solid\" || value === \"dashed\" || value === \"dotted\")\n return value;\n return fallback;\n}\n\nfunction makeStyles(values: UnlayerContentValues): Block[\"styles\"] {\n const padding = parsePaddingShorthand(values.containerPadding);\n return {\n padding,\n };\n}\n\n/**\n * Apply Unlayer text-level styles as TipTap-compatible inline markup.\n * Mirrors the BeeFree importer's helper but reads from Unlayer's flat\n * values shape rather than a CSS style record.\n */\nfunction inlineStylesToHtml(\n html: string,\n values: UnlayerContentValues,\n): string {\n const spanParts: string[] = [];\n const fontSize = parsePxValue(values.fontSize);\n if (fontSize && fontSize !== 16) spanParts.push(`font-size: ${fontSize}px`);\n const color = parseColor(values.color);\n if (color && color !== \"#1a1a1a\") spanParts.push(`color: ${color}`);\n const fontWeight = values.fontWeight;\n if (\n fontWeight !== undefined &&\n fontWeight !== null &&\n String(fontWeight) !== \"normal\" &&\n String(fontWeight) !== \"400\"\n ) {\n spanParts.push(`font-weight: ${fontWeight}`);\n }\n const fontFamily = parseFontFamily(values.fontFamily);\n if (fontFamily) spanParts.push(`font-family: ${fontFamily}`);\n\n const textAlign = values.textAlign;\n const pStyle =\n textAlign && textAlign !== \"left\" ? `text-align: ${textAlign}` : \"\";\n\n if (!pStyle && spanParts.length === 0) return html;\n\n const spanStyle = spanParts.join(\"; \");\n let result = html;\n\n if (pStyle) {\n result = result\n .replace(/<p style=\"([^\"]*)\">/g, `<p style=\"$1; ${pStyle}\">`)\n .replaceAll(\"<p>\", `<p style=\"${pStyle}\">`);\n }\n\n if (spanStyle) {\n result = wrapParagraphInner(result, spanStyle);\n }\n\n return result;\n}\n\n/**\n * Wrap the inner content of every `<p …>…</p>` with `<span style=\"…\">…</span>`.\n *\n * Hand-rolled linear scanner instead of `/<p([^>]*)>([\\s\\S]*?)<\\/p>/g` because\n * the regex is polynomial-ReDoS: the engine retries `[\\s\\S]*?<\\/p>` at every\n * `<p` start, so inputs like `<p>a<p>a<p>a…` (no `</p>` ever) cost O(n²).\n * The scanner advances `i` monotonically via `indexOf`, keeping the work\n * strictly O(n).\n */\nfunction wrapParagraphInner(html: string, spanStyle: string): string {\n let out = \"\";\n let i = 0;\n while (i < html.length) {\n const open = html.indexOf(\"<p\", i);\n if (open === -1) {\n out += html.substring(i);\n break;\n }\n const afterTagName = html[open + 2];\n if (\n afterTagName !== \">\" &&\n afterTagName !== \" \" &&\n afterTagName !== \"\\t\" &&\n afterTagName !== \"\\n\" &&\n afterTagName !== \"\\r\" &&\n afterTagName !== \"/\"\n ) {\n out += html.substring(i, open + 2);\n i = open + 2;\n continue;\n }\n const openEnd = html.indexOf(\">\", open + 2);\n if (openEnd === -1) {\n out += html.substring(i);\n break;\n }\n const closeStart = html.indexOf(\"</p>\", openEnd + 1);\n if (closeStart === -1) {\n out += html.substring(i);\n break;\n }\n const inner = html.substring(openEnd + 1, closeStart);\n out += html.substring(i, openEnd + 1);\n out += `<span style=\"${spanStyle}\">${inner}</span></p>`;\n i = closeStart + 4;\n }\n return out;\n}\n\nfunction ensureParagraphWrapped(html: string): string {\n if (!html) return \"<p></p>\";\n if (/<p[\\s>]/i.test(html)) return html;\n return `<p>${html}</p>`;\n}\n\nfunction convertText(values: UnlayerContentValues): Block {\n const html = ensureParagraphWrapped(values.text ?? \"\");\n\n return createParagraphBlock({\n content: inlineStylesToHtml(html, values),\n styles: makeStyles(values),\n });\n}\n\nfunction parseHeadingLevel(tag: string | undefined): HeadingLevel {\n if (!tag) return 2;\n const match = tag.match(/^h(\\d)$/i);\n if (match) {\n const num = Number(match[1]);\n if (num >= 1 && num <= 4) return num as HeadingLevel;\n }\n return 2;\n}\n\nfunction convertHeading(values: UnlayerContentValues): Block {\n const text = values.text ?? \"\";\n const stripped = text.replace(/^<h\\d[^>]*>|<\\/h\\d>$/gi, \"\");\n const content = stripped ? `<p>${stripped}</p>` : \"<p></p>\";\n\n return createTitleBlock({\n content,\n level: parseHeadingLevel(values.headingType),\n color: parseColor(values.color) || \"#1a1a1a\",\n textAlign: toAlign(values.textAlign),\n fontFamily: parseFontFamily(values.fontFamily) || undefined,\n styles: makeStyles(values),\n });\n}\n\nfunction convertImage(values: UnlayerContentValues): Block {\n const src = values.src;\n const action = values.action?.values;\n\n return createImageBlock({\n src: src?.url || \"\",\n alt: values.altText || \"\",\n width: src?.width ? Math.round(src.width) : 600,\n align: toAlign(values.textAlign, \"center\"),\n linkUrl: action?.href || undefined,\n linkOpenInNewTab: action?.target === \"_blank\" || undefined,\n styles: makeStyles(values),\n });\n}\n\nfunction convertButton(values: UnlayerContentValues): Block {\n const colors = values.buttonColors ?? {};\n const padding = values.padding\n ? parsePaddingShorthand(values.padding)\n : { top: 12, right: 24, bottom: 12, left: 24 };\n const label = stripTagsPlain(values.text ?? \"Button\");\n const linkValues = values.href?.values;\n\n return createButtonBlock({\n text: label,\n url: linkValues?.href || \"#\",\n openInNewTab: linkValues?.target === \"_blank\" || undefined,\n backgroundColor: parseColor(colors.backgroundColor) || \"#4f46e5\",\n textColor: parseColor(colors.color) || \"#ffffff\",\n borderRadius: parsePxValue(values.borderRadius),\n fontSize: parsePxValue(values.fontSize) || 16,\n fontFamily: parseFontFamily(values.fontFamily) || undefined,\n buttonPadding: padding,\n styles: makeStyles(values),\n });\n}\n\nfunction convertDivider(values: UnlayerContentValues): Block {\n const border = parseBorderObject(values.border);\n\n return createDividerBlock({\n lineStyle: toLineStyle(border.style),\n color: border.color,\n thickness: border.width || 1,\n width: parseWidthPercent(values.width),\n styles: makeStyles(values),\n });\n}\n\nfunction convertSpacer(values: UnlayerContentValues): Block {\n const padding = parsePaddingShorthand(values.containerPadding);\n const height =\n parsePxValue((values as { height?: string | number }).height) ||\n padding.top + padding.bottom ||\n 24;\n\n return createSpacerBlock({\n height,\n styles: makeStyles(values),\n });\n}\n\nfunction convertHtml(values: UnlayerContentValues): Block {\n return createHtmlBlock({\n content: values.html ?? \"\",\n styles: makeStyles(values),\n });\n}\n\nfunction convertSocial(\n values: UnlayerContentValues,\n warnings: string[],\n): Block {\n const iconList = values.icons?.icons ?? [];\n const icons: SocialIcon[] = [];\n\n for (const unlayerIcon of iconList) {\n const id = (unlayerIcon.name ?? \"\").toLowerCase();\n const platform = SOCIAL_PLATFORM_MAP[id];\n\n if (!platform) {\n warnings.push(\n `Unrecognized social icon \"${unlayerIcon.name || id}\" was skipped.`,\n );\n continue;\n }\n\n icons.push({\n id: generateId(),\n platform,\n url: unlayerIcon.url || \"#\",\n });\n }\n\n return createSocialIconsBlock({\n icons,\n align: toAlign(values.textAlign, \"center\"),\n styles: makeStyles(values),\n });\n}\n\nfunction convertVideo(values: UnlayerContentValues): Block {\n return createVideoBlock({\n url: values.videoUrl || \"\",\n thumbnailUrl: values.thumbnailUrl || \"\",\n alt: values.altText || \"\",\n width: 600,\n align: toAlign(values.textAlign, \"center\"),\n styles: makeStyles(values),\n });\n}\n\nfunction convertMenu(values: UnlayerContentValues): Block {\n const menu = values.menu;\n const items: MenuItemData[] = (menu?.items ?? []).map((item) => ({\n id: generateId(),\n text: item.text || \"\",\n url: item.link?.values?.href || \"#\",\n openInNewTab: item.link?.values?.target === \"_blank\",\n bold: false,\n underline: false,\n }));\n\n return createMenuBlock({\n items,\n separator: values.separator || \"|\",\n separatorColor: \"#999999\",\n fontSize: parsePxValue(values.fontSize) || 14,\n color: parseColor(values.color) || \"#1a1a1a\",\n fontFamily: parseFontFamily(values.fontFamily) || undefined,\n textAlign: toAlign(values.textAlign, \"center\"),\n styles: makeStyles(values),\n });\n}\n\nfunction convertHtmlFallback(content: UnlayerContent, comment: string): Block {\n const safe = comment.replace(/</g, \"<\").replace(/>/g, \">\");\n return createHtmlBlock({\n content: `<div style=\"padding:12px;border:1px dashed #d1d5db;border-radius:6px;background:#fafafa;color:#6b7280;font-family:sans-serif;font-size:13px;\">${safe}</div>`,\n styles: makeStyles(content.values),\n });\n}\n\n/**\n * Converts a single Unlayer content node to a Templatical block.\n * Returns the block and a report entry.\n */\nexport function convertContent(\n content: UnlayerContent,\n warnings: string[],\n): { block: Block; entry: ImportReportEntry } {\n const type = content.type;\n const values = content.values ?? ({} as UnlayerContentValues);\n\n switch (type) {\n case \"text\":\n return {\n block: convertText(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n case \"heading\":\n return {\n block: convertHeading(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"title\",\n status: \"converted\",\n },\n };\n case \"image\":\n return {\n block: convertImage(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"image\",\n status: \"converted\",\n },\n };\n case \"button\":\n return {\n block: convertButton(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"button\",\n status: \"converted\",\n },\n };\n case \"divider\":\n return {\n block: convertDivider(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"divider\",\n status: \"converted\",\n },\n };\n case \"spacer\":\n return {\n block: convertSpacer(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"spacer\",\n status: \"converted\",\n },\n };\n case \"html\":\n return {\n block: convertHtml(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"html\",\n status: \"converted\",\n },\n };\n case \"menu\":\n return {\n block: convertMenu(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"menu\",\n status: \"approximated\",\n note: \"Menu styles map approximately; review spacing and separator color.\",\n },\n };\n case \"social\":\n return {\n block: convertSocial(values, warnings),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"social\",\n status: \"converted\",\n },\n };\n case \"video\":\n return {\n block: convertVideo(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"video\",\n status: \"converted\",\n },\n };\n case \"timer\":\n return {\n block: convertHtmlFallback(\n content,\n \"Unlayer timer block: rebuild manually in Templatical\",\n ),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: \"Timer modules have no direct Templatical equivalent; placeholder HTML inserted.\",\n },\n };\n case \"form\":\n return {\n block: convertHtmlFallback(\n content,\n \"Unlayer form block: not supported in Templatical (most email clients block form submission)\",\n ),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: null,\n status: \"skipped\",\n note: \"Unlayer forms have no Templatical equivalent and are skipped. Most email clients block form submission anyway.\",\n },\n };\n default:\n return {\n block: convertHtmlFallback(\n content,\n `Unsupported Unlayer content type: ${type}`,\n ),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `Unknown content type \"${type}\" converted to HTML block.`,\n },\n };\n }\n}\n","import type { SpacingValue } from \"@templatical/types\";\nimport type { UnlayerBorder, UnlayerFontFamily } from \"./types\";\n\n/**\n * Parses CSS-like style values from Unlayer content values.\n */\n\nexport function parsePxValue(value: string | number | undefined): number {\n if (value === undefined || value === null) return 0;\n if (typeof value === \"number\") return Math.round(value);\n // Single trailing-whitespace group with an optional `px` inside it. The\n // original `\\s*(?:px)?\\s*` had two `\\s*` quantifiers around an optional\n // group, which is the textbook polynomial-ReDoS shape (\"how should the\n // whitespace split across the two quantifiers?\") over inputs like\n // `9 ` with no `px`.\n const match = value.match(/^(-?\\d+(?:\\.\\d+)?)\\s*(?:px\\s*)?$/);\n return match ? Math.round(parseFloat(match[1])) : 0;\n}\n\nexport function parseColor(value: string | undefined): string {\n if (!value || value === \"transparent\") return \"\";\n\n const trimmed = value.trim();\n\n if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed.toLowerCase();\n\n if (/^#[0-9a-fA-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}`.toLowerCase();\n }\n\n return trimmed;\n}\n\nexport function parsePaddingShorthand(value: string | undefined): SpacingValue {\n if (!value) return { top: 0, right: 0, bottom: 0, left: 0 };\n\n const parts = value.trim().split(/\\s+/);\n const values = parts.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\nexport function parseBorderObject(border: UnlayerBorder | undefined): {\n width: number;\n style: string;\n color: string;\n} {\n if (!border) return { width: 0, style: \"solid\", color: \"#000000\" };\n return {\n width: parsePxValue(border.borderTopWidth),\n style: border.borderTopStyle || \"solid\",\n color: parseColor(border.borderTopColor) || \"#000000\",\n };\n}\n\nexport function parseWidthPercent(value: string | undefined): number {\n if (!value) return 100;\n const match = value.match(/^(\\d+(?:\\.\\d+)?)\\s*%/);\n if (match) return Math.round(parseFloat(match[1]));\n return 100;\n}\n\nexport function parseFontFamily(\n value: UnlayerFontFamily | string | undefined,\n): string {\n if (!value) return \"\";\n if (typeof value === \"string\") {\n return value.split(\",\")[0].trim().replace(/['\"]/g, \"\");\n }\n if (value.value) return value.value.split(\",\")[0].trim().replace(/['\"]/g, \"\");\n if (value.label) return value.label;\n return \"\";\n}\n"],"mappings":"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","names":[]} | ||
| {"version":3,"file":"index.js","names":[],"sources":["../src/style-parser.ts","../src/block-mapper.ts","../src/converter.ts"],"sourcesContent":["import type { SpacingValue } from \"@templatical/types\";\nimport type { UnlayerBorder, UnlayerFontFamily } from \"./types\";\n\n/**\n * Parses CSS-like style values from Unlayer content values.\n */\n\nexport function parsePxValue(value: string | number | undefined): number {\n if (value === undefined || value === null) return 0;\n if (typeof value === \"number\") return Math.round(value);\n // Single trailing-whitespace group with an optional `px` inside it. The\n // original `\\s*(?:px)?\\s*` had two `\\s*` quantifiers around an optional\n // group, which is the textbook polynomial-ReDoS shape (\"how should the\n // whitespace split across the two quantifiers?\") over inputs like\n // `9 ` with no `px`.\n const match = value.match(/^(-?\\d+(?:\\.\\d+)?)\\s*(?:px\\s*)?$/);\n return match ? Math.round(parseFloat(match[1])) : 0;\n}\n\nexport function parseColor(value: string | undefined): string {\n if (!value || value === \"transparent\") return \"\";\n\n const trimmed = value.trim();\n\n if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed.toLowerCase();\n\n if (/^#[0-9a-fA-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}`.toLowerCase();\n }\n\n return trimmed;\n}\n\nexport function parsePaddingShorthand(value: string | undefined): SpacingValue {\n if (!value) return { top: 0, right: 0, bottom: 0, left: 0 };\n\n const parts = value.trim().split(/\\s+/);\n const values = parts.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\nexport function parseBorderObject(border: UnlayerBorder | undefined): {\n width: number;\n style: string;\n color: string;\n} {\n if (!border) return { width: 0, style: \"solid\", color: \"#000000\" };\n return {\n width: parsePxValue(border.borderTopWidth),\n style: border.borderTopStyle || \"solid\",\n color: parseColor(border.borderTopColor) || \"#000000\",\n };\n}\n\nexport function parseWidthPercent(value: string | undefined): number {\n if (!value) return 100;\n const match = value.match(/^(\\d+(?:\\.\\d+)?)\\s*%/);\n if (match) return Math.round(parseFloat(match[1]));\n return 100;\n}\n\nexport function parseFontFamily(\n value: UnlayerFontFamily | string | undefined,\n): string {\n if (!value) return \"\";\n if (typeof value === \"string\") {\n return value.split(\",\")[0].trim().replace(/['\"]/g, \"\");\n }\n if (value.value) return value.value.split(\",\")[0].trim().replace(/['\"]/g, \"\");\n if (value.label) return value.label;\n return \"\";\n}\n","import {\n createTitleBlock,\n createParagraphBlock,\n createImageBlock,\n createButtonBlock,\n createDividerBlock,\n createSpacerBlock,\n createHtmlBlock,\n createSocialIconsBlock,\n createMenuBlock,\n createVideoBlock,\n generateId,\n} from \"@templatical/types\";\nimport type {\n Block,\n HeadingLevel,\n SocialPlatform,\n SocialIcon,\n MenuItemData,\n} from \"@templatical/types\";\nimport type {\n UnlayerContent,\n UnlayerContentValues,\n ImportReportEntry,\n} from \"./types\";\nimport {\n parsePxValue,\n parseColor,\n parseFontFamily,\n parsePaddingShorthand,\n parseBorderObject,\n parseWidthPercent,\n} from \"./style-parser\";\n\nconst SOCIAL_PLATFORM_MAP: Record<string, SocialPlatform> = {\n facebook: \"facebook\",\n twitter: \"twitter\",\n x: \"twitter\",\n instagram: \"instagram\",\n linkedin: \"linkedin\",\n youtube: \"youtube\",\n tiktok: \"tiktok\",\n pinterest: \"pinterest\",\n email: \"email\",\n mail: \"email\",\n whatsapp: \"whatsapp\",\n telegram: \"telegram\",\n discord: \"discord\",\n snapchat: \"snapchat\",\n reddit: \"reddit\",\n github: \"github\",\n dribbble: \"dribbble\",\n behance: \"behance\",\n};\n\ntype Align = \"left\" | \"center\" | \"right\";\ntype LineStyle = \"solid\" | \"dashed\" | \"dotted\";\n\nfunction toAlign(value: string | undefined, fallback: Align = \"left\"): Align {\n if (value === \"left\" || value === \"center\" || value === \"right\") return value;\n return fallback;\n}\n\n/**\n * Strip every `<…>` from a button label and reject any stray `<` or `>`\n * left behind (e.g. a truncated `<script`). The original\n * `value.replace(/<[^>]*>/g, \"\")` was both polynomial-ReDoS over\n * `<<<<…` inputs and an incomplete sanitizer — an unterminated `<script`\n * would survive the strip. Downstream HTML-escapes the label at render\n * time, but stripping here keeps the imported JSON clean.\n */\nfunction stripTagsPlain(text: string): string {\n let out = \"\";\n let i = 0;\n while (i < text.length) {\n if (text[i] === \"<\") {\n const close = text.indexOf(\">\", i + 1);\n if (close === -1) {\n // Unterminated tag — discard the rest.\n break;\n }\n i = close + 1;\n continue;\n }\n if (text[i] === \">\") {\n i++;\n continue;\n }\n out += text[i];\n i++;\n }\n return out;\n}\n\nfunction toLineStyle(\n value: string | undefined,\n fallback: LineStyle = \"solid\",\n): LineStyle {\n if (value === \"solid\" || value === \"dashed\" || value === \"dotted\")\n return value;\n return fallback;\n}\n\nfunction makeStyles(values: UnlayerContentValues): Block[\"styles\"] {\n const padding = parsePaddingShorthand(values.containerPadding);\n return {\n padding,\n };\n}\n\n/**\n * Apply Unlayer text-level styles as TipTap-compatible inline markup.\n * Mirrors the BeeFree importer's helper but reads from Unlayer's flat\n * values shape rather than a CSS style record.\n */\nfunction inlineStylesToHtml(\n html: string,\n values: UnlayerContentValues,\n): string {\n const spanParts: string[] = [];\n const fontSize = parsePxValue(values.fontSize);\n if (fontSize && fontSize !== 16) spanParts.push(`font-size: ${fontSize}px`);\n const color = parseColor(values.color);\n if (color && color !== \"#1a1a1a\") spanParts.push(`color: ${color}`);\n const fontWeight = values.fontWeight;\n if (\n fontWeight !== undefined &&\n fontWeight !== null &&\n String(fontWeight) !== \"normal\" &&\n String(fontWeight) !== \"400\"\n ) {\n spanParts.push(`font-weight: ${fontWeight}`);\n }\n const fontFamily = parseFontFamily(values.fontFamily);\n if (fontFamily) spanParts.push(`font-family: ${fontFamily}`);\n\n const textAlign = values.textAlign;\n const pStyle =\n textAlign && textAlign !== \"left\" ? `text-align: ${textAlign}` : \"\";\n\n if (!pStyle && spanParts.length === 0) return html;\n\n const spanStyle = spanParts.join(\"; \");\n let result = html;\n\n if (pStyle) {\n result = result\n .replace(/<p style=\"([^\"]*)\">/g, `<p style=\"$1; ${pStyle}\">`)\n .replaceAll(\"<p>\", `<p style=\"${pStyle}\">`);\n }\n\n if (spanStyle) {\n result = wrapParagraphInner(result, spanStyle);\n }\n\n return result;\n}\n\n/**\n * Wrap the inner content of every `<p …>…</p>` with `<span style=\"…\">…</span>`.\n *\n * Hand-rolled linear scanner instead of `/<p([^>]*)>([\\s\\S]*?)<\\/p>/g` because\n * the regex is polynomial-ReDoS: the engine retries `[\\s\\S]*?<\\/p>` at every\n * `<p` start, so inputs like `<p>a<p>a<p>a…` (no `</p>` ever) cost O(n²).\n * The scanner advances `i` monotonically via `indexOf`, keeping the work\n * strictly O(n).\n */\nfunction wrapParagraphInner(html: string, spanStyle: string): string {\n let out = \"\";\n let i = 0;\n while (i < html.length) {\n const open = html.indexOf(\"<p\", i);\n if (open === -1) {\n out += html.substring(i);\n break;\n }\n const afterTagName = html[open + 2];\n if (\n afterTagName !== \">\" &&\n afterTagName !== \" \" &&\n afterTagName !== \"\\t\" &&\n afterTagName !== \"\\n\" &&\n afterTagName !== \"\\r\" &&\n afterTagName !== \"/\"\n ) {\n out += html.substring(i, open + 2);\n i = open + 2;\n continue;\n }\n const openEnd = html.indexOf(\">\", open + 2);\n if (openEnd === -1) {\n out += html.substring(i);\n break;\n }\n const closeStart = html.indexOf(\"</p>\", openEnd + 1);\n if (closeStart === -1) {\n out += html.substring(i);\n break;\n }\n const inner = html.substring(openEnd + 1, closeStart);\n out += html.substring(i, openEnd + 1);\n out += `<span style=\"${spanStyle}\">${inner}</span></p>`;\n i = closeStart + 4;\n }\n return out;\n}\n\nfunction ensureParagraphWrapped(html: string): string {\n if (!html) return \"<p></p>\";\n if (/<p[\\s>]/i.test(html)) return html;\n return `<p>${html}</p>`;\n}\n\nfunction convertText(values: UnlayerContentValues): Block {\n const html = ensureParagraphWrapped(values.text ?? \"\");\n\n return createParagraphBlock({\n content: inlineStylesToHtml(html, values),\n styles: makeStyles(values),\n });\n}\n\nfunction parseHeadingLevel(tag: string | undefined): HeadingLevel {\n if (!tag) return 2;\n const match = tag.match(/^h(\\d)$/i);\n if (match) {\n const num = Number(match[1]);\n if (num >= 1 && num <= 4) return num as HeadingLevel;\n }\n return 2;\n}\n\nfunction convertHeading(values: UnlayerContentValues): Block {\n const text = values.text ?? \"\";\n const stripped = text.replace(/^<h\\d[^>]*>|<\\/h\\d>$/gi, \"\");\n const content = stripped ? `<p>${stripped}</p>` : \"<p></p>\";\n\n return createTitleBlock({\n content,\n level: parseHeadingLevel(values.headingType),\n color: parseColor(values.color) || \"#1a1a1a\",\n textAlign: toAlign(values.textAlign),\n fontFamily: parseFontFamily(values.fontFamily) || undefined,\n styles: makeStyles(values),\n });\n}\n\nfunction convertImage(values: UnlayerContentValues): Block {\n const src = values.src;\n const action = values.action?.values;\n\n return createImageBlock({\n src: src?.url || \"\",\n alt: values.altText || \"\",\n width: src?.width ? Math.round(src.width) : 600,\n align: toAlign(values.textAlign, \"center\"),\n linkUrl: action?.href || undefined,\n linkOpenInNewTab: action?.target === \"_blank\" || undefined,\n styles: makeStyles(values),\n });\n}\n\nfunction convertButton(values: UnlayerContentValues): Block {\n const colors = values.buttonColors ?? {};\n const padding = values.padding\n ? parsePaddingShorthand(values.padding)\n : { top: 12, right: 24, bottom: 12, left: 24 };\n const label = stripTagsPlain(values.text ?? \"Button\");\n const linkValues = values.href?.values;\n\n return createButtonBlock({\n text: label,\n url: linkValues?.href || \"#\",\n openInNewTab: linkValues?.target === \"_blank\" || undefined,\n backgroundColor: parseColor(colors.backgroundColor) || \"#4f46e5\",\n textColor: parseColor(colors.color) || \"#ffffff\",\n borderRadius: parsePxValue(values.borderRadius),\n fontSize: parsePxValue(values.fontSize) || 16,\n fontFamily: parseFontFamily(values.fontFamily) || undefined,\n buttonPadding: padding,\n styles: makeStyles(values),\n });\n}\n\nfunction convertDivider(values: UnlayerContentValues): Block {\n const border = parseBorderObject(values.border);\n\n return createDividerBlock({\n lineStyle: toLineStyle(border.style),\n color: border.color,\n thickness: border.width || 1,\n width: parseWidthPercent(values.width),\n styles: makeStyles(values),\n });\n}\n\nfunction convertSpacer(values: UnlayerContentValues): Block {\n const padding = parsePaddingShorthand(values.containerPadding);\n const height =\n parsePxValue((values as { height?: string | number }).height) ||\n padding.top + padding.bottom ||\n 24;\n\n return createSpacerBlock({\n height,\n styles: makeStyles(values),\n });\n}\n\nfunction convertHtml(values: UnlayerContentValues): Block {\n return createHtmlBlock({\n content: values.html ?? \"\",\n styles: makeStyles(values),\n });\n}\n\nfunction convertSocial(\n values: UnlayerContentValues,\n warnings: string[],\n): Block {\n const iconList = values.icons?.icons ?? [];\n const icons: SocialIcon[] = [];\n\n for (const unlayerIcon of iconList) {\n const id = (unlayerIcon.name ?? \"\").toLowerCase();\n const platform = SOCIAL_PLATFORM_MAP[id];\n\n if (!platform) {\n warnings.push(\n `Unrecognized social icon \"${unlayerIcon.name || id}\" was skipped.`,\n );\n continue;\n }\n\n icons.push({\n id: generateId(),\n platform,\n url: unlayerIcon.url || \"#\",\n });\n }\n\n return createSocialIconsBlock({\n icons,\n align: toAlign(values.textAlign, \"center\"),\n styles: makeStyles(values),\n });\n}\n\nfunction convertVideo(values: UnlayerContentValues): Block {\n return createVideoBlock({\n url: values.videoUrl || \"\",\n thumbnailUrl: values.thumbnailUrl || \"\",\n alt: values.altText || \"\",\n width: 600,\n align: toAlign(values.textAlign, \"center\"),\n styles: makeStyles(values),\n });\n}\n\nfunction convertMenu(values: UnlayerContentValues): Block {\n const menu = values.menu;\n const items: MenuItemData[] = (menu?.items ?? []).map((item) => ({\n id: generateId(),\n text: item.text || \"\",\n url: item.link?.values?.href || \"#\",\n openInNewTab: item.link?.values?.target === \"_blank\",\n bold: false,\n underline: false,\n }));\n\n return createMenuBlock({\n items,\n separator: values.separator || \"|\",\n separatorColor: \"#999999\",\n fontSize: parsePxValue(values.fontSize) || 14,\n color: parseColor(values.color) || \"#1a1a1a\",\n fontFamily: parseFontFamily(values.fontFamily) || undefined,\n textAlign: toAlign(values.textAlign, \"center\"),\n styles: makeStyles(values),\n });\n}\n\nfunction convertHtmlFallback(content: UnlayerContent, comment: string): Block {\n const safe = comment.replace(/</g, \"<\").replace(/>/g, \">\");\n return createHtmlBlock({\n content: `<div style=\"padding:12px;border:1px dashed #d1d5db;border-radius:6px;background:#fafafa;color:#6b7280;font-family:sans-serif;font-size:13px;\">${safe}</div>`,\n styles: makeStyles(content.values),\n });\n}\n\n/**\n * Converts a single Unlayer content node to a Templatical block.\n * Returns the block and a report entry.\n */\nexport function convertContent(\n content: UnlayerContent,\n warnings: string[],\n): { block: Block; entry: ImportReportEntry } {\n const type = content.type;\n const values = content.values ?? ({} as UnlayerContentValues);\n\n switch (type) {\n case \"text\":\n return {\n block: convertText(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n case \"heading\":\n return {\n block: convertHeading(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"title\",\n status: \"converted\",\n },\n };\n case \"image\":\n return {\n block: convertImage(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"image\",\n status: \"converted\",\n },\n };\n case \"button\":\n return {\n block: convertButton(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"button\",\n status: \"converted\",\n },\n };\n case \"divider\":\n return {\n block: convertDivider(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"divider\",\n status: \"converted\",\n },\n };\n case \"spacer\":\n return {\n block: convertSpacer(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"spacer\",\n status: \"converted\",\n },\n };\n case \"html\":\n return {\n block: convertHtml(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"html\",\n status: \"converted\",\n },\n };\n case \"menu\":\n return {\n block: convertMenu(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"menu\",\n status: \"approximated\",\n note: \"Menu styles map approximately; review spacing and separator color.\",\n },\n };\n case \"social\":\n return {\n block: convertSocial(values, warnings),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"social\",\n status: \"converted\",\n },\n };\n case \"video\":\n return {\n block: convertVideo(values),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"video\",\n status: \"converted\",\n },\n };\n case \"timer\":\n return {\n block: convertHtmlFallback(\n content,\n \"Unlayer timer block: rebuild manually in Templatical\",\n ),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: \"Timer modules have no direct Templatical equivalent; placeholder HTML inserted.\",\n },\n };\n case \"form\":\n return {\n block: convertHtmlFallback(\n content,\n \"Unlayer form block: not supported in Templatical (most email clients block form submission)\",\n ),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: null,\n status: \"skipped\",\n note: \"Unlayer forms have no Templatical equivalent and are skipped. Most email clients block form submission anyway.\",\n },\n };\n default:\n return {\n block: convertHtmlFallback(\n content,\n `Unsupported Unlayer content type: ${type}`,\n ),\n entry: {\n unlayerContentType: type,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `Unknown content type \"${type}\" converted to HTML block.`,\n },\n };\n }\n}\n","import {\n createSectionBlock,\n createDefaultTemplateContent,\n} from \"@templatical/types\";\nimport type { Block, ColumnLayout, TemplateContent } from \"@templatical/types\";\nimport type {\n UnlayerTemplate,\n UnlayerRow,\n UnlayerColumn,\n ImportResult,\n ImportReport,\n ImportReportEntry,\n} from \"./types\";\nimport { convertContent } from \"./block-mapper\";\nimport {\n parsePxValue,\n parseColor,\n parseFontFamily,\n parsePaddingShorthand,\n} from \"./style-parser\";\n\nfunction resolveColumnLayout(\n cells: number[],\n warnings: string[],\n): ColumnLayout {\n if (cells.length <= 1) return \"1\";\n if (cells.length === 3) return \"3\";\n\n if (cells.length === 2) {\n const left = cells[0] ?? 1;\n const right = cells[1] ?? 1;\n const total = left + right;\n const ratio = left / total;\n\n if (ratio > 0.58) return \"2-1\";\n if (ratio < 0.42) return \"1-2\";\n return \"2\";\n }\n\n warnings.push(\n `Row with ${cells.length} columns was flattened to a single column. Unlayer supports arbitrary columns, but Templatical supports up to 3.`,\n );\n return \"1\";\n}\n\n/**\n * Converts all contents in a column to Templatical blocks.\n */\nfunction convertColumnContents(\n column: UnlayerColumn,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n const blocks: Block[] = [];\n\n for (const content of column.contents ?? []) {\n const { block, entry } = convertContent(content, warnings);\n blocks.push(block);\n entries.push(entry);\n }\n\n return blocks;\n}\n\n/**\n * Processes a single Unlayer row into one or more Templatical blocks.\n */\nfunction processRow(\n row: UnlayerRow,\n entries: ImportReportEntry[],\n warnings: string[],\n): Block[] {\n const columns = row.columns;\n if (!columns || columns.length === 0) return [];\n\n const cells = row.cells ?? columns.map(() => 1);\n const layout = resolveColumnLayout(cells, warnings);\n\n let children: Block[][];\n if (layout === \"1\") {\n const merged: Block[] = [];\n for (const column of columns) {\n merged.push(...convertColumnContents(column, entries, warnings));\n }\n children = [merged];\n } else {\n children = columns.map((col) =>\n convertColumnContents(col, entries, warnings),\n );\n }\n\n const rowBg = parseColor(row.values?.backgroundColor);\n const padding = parsePaddingShorthand(row.values?.padding);\n\n const section = createSectionBlock({\n columns: layout,\n children,\n styles: {\n padding,\n ...(rowBg ? { backgroundColor: rowBg } : {}),\n },\n });\n\n return [section];\n}\n\n/**\n * Extracts template-level settings from the Unlayer body values.\n */\nfunction extractSettings(\n template: UnlayerTemplate,\n): TemplateContent[\"settings\"] {\n const values = template.body?.values ?? {};\n\n const width = parsePxValue(values.contentWidth);\n const bgColor = parseColor(values.backgroundColor) || \"#ffffff\";\n const fontFamily = parseFontFamily(values.fontFamily) || \"Arial\";\n\n return {\n width: width || 600,\n backgroundColor: bgColor,\n fontFamily,\n locale: \"en\",\n };\n}\n\n/**\n * Converts an Unlayer design JSON to Templatical TemplateContent.\n *\n * @param template - The parsed Unlayer JSON object (the result of `editor.saveDesign(...)`)\n * @returns An ImportResult with the converted content and a detailed report\n *\n * @example\n * ```ts\n * import { convertUnlayerTemplate } from '@templatical/import-unlayer';\n *\n * const unlayerJson = JSON.parse(fileContent);\n * const { content, report } = convertUnlayerTemplate(unlayerJson);\n *\n * const editor = init({ container: '#editor', content });\n *\n * console.log(report.summary);\n * console.log(report.warnings);\n * ```\n */\nexport function convertUnlayerTemplate(\n template: UnlayerTemplate,\n): ImportResult {\n if (!template?.body?.rows) {\n throw new Error(\n \"Invalid Unlayer template: missing body.rows. Ensure you are passing a valid Unlayer JSON design (the output of editor.saveDesign).\",\n );\n }\n\n const entries: ImportReportEntry[] = [];\n const warnings: string[] = [];\n const blocks: Block[] = [];\n\n const headers = template.body.headers ?? [];\n const footers = template.body.footers ?? [];\n\n if (headers.length > 0) {\n warnings.push(\n `${headers.length} Unlayer header row(s) were imported as regular rows at the top of the template.`,\n );\n for (const row of headers) {\n blocks.push(...processRow(row, entries, warnings));\n }\n }\n\n for (const row of template.body.rows) {\n blocks.push(...processRow(row, entries, warnings));\n }\n\n if (footers.length > 0) {\n warnings.push(\n `${footers.length} Unlayer footer row(s) were imported as regular rows at the bottom of the template.`,\n );\n for (const row of footers) {\n blocks.push(...processRow(row, entries, warnings));\n }\n }\n\n const content: TemplateContent = {\n ...createDefaultTemplateContent(),\n blocks,\n settings: extractSettings(template),\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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"} |
+3
-4
| { | ||
| "name": "@templatical/import-unlayer", | ||
| "description": "Convert Unlayer email templates to Templatical format", | ||
| "version": "0.10.0", | ||
| "version": "0.10.1", | ||
| "bugs": "https://github.com/templatical/sdk/issues", | ||
| "dependencies": { | ||
| "@templatical/types": "0.10.0" | ||
| "@templatical/types": "0.10.1" | ||
| }, | ||
| "devDependencies": { | ||
| "tsup": "^8.5.1", | ||
| "typescript": "^6.0.3", | ||
@@ -45,3 +44,3 @@ "vitest": "^4.1.7" | ||
| "scripts": { | ||
| "build": "tsup", | ||
| "build": "tsdown", | ||
| "test": "vitest run --config vitest.config.ts", | ||
@@ -48,0 +47,0 @@ "typecheck": "tsc --noEmit" |
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
2
-33.33%64099
-3.28%805
-1.71%+ Added
- Removed
Updated