@templatical/import-stripo 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +627 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Templatical
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @templatical/import-stripo
|
|
2
|
+
|
|
3
|
+
Convert Stripo email templates to Templatical format.
|
|
4
|
+
|
|
5
|
+
Reads either surface Stripo produces:
|
|
6
|
+
|
|
7
|
+
- **Plugin / editor storage** — `{ html, css }` from `getTemplateData()` (`esd-*` class attributes).
|
|
8
|
+
- **Compiled export** — File → HTML (`es-wrapper` / `es-left` / `es-button`, no `esd-*` on elements).
|
|
9
|
+
|
|
10
|
+
The converter auto-detects which one you passed. There is no mode flag.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @templatical/import-stripo
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { convertStripoTemplate } from "@templatical/import-stripo";
|
|
22
|
+
|
|
23
|
+
// Plugin host (what you stored from getTemplateData):
|
|
24
|
+
const { content, report } = convertStripoTemplate(html, { css });
|
|
25
|
+
|
|
26
|
+
// Marketer File → HTML export:
|
|
27
|
+
const exported = convertStripoTemplate(fileHtml);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
See [Migrating from Stripo](https://docs.templatical.com/guide/migration-from-stripo).
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ConversionStatus, ImportReport, ImportReportEntry, ImportResult, ImportResult as ImportResult$1 } from "@templatical/import-html";
|
|
2
|
+
//#region src/converter.d.ts
|
|
3
|
+
interface ConvertStripoOptions {
|
|
4
|
+
/** Sibling CSS from plugin `getTemplateData()`. Ignored on compiled exports. */
|
|
5
|
+
css?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Convert a Stripo template to Templatical content.
|
|
9
|
+
*
|
|
10
|
+
* Auto-detects the surface from class attributes:
|
|
11
|
+
* - `esd-*` on elements → plugin/editor `getTemplateData` HTML
|
|
12
|
+
* - `es-wrapper` / `es-*-body` without `esd-*` → compiled File→HTML export
|
|
13
|
+
*
|
|
14
|
+
* Pass `options.css` when converting plugin storage (`getTemplateData().css`).
|
|
15
|
+
*/
|
|
16
|
+
export declare function convertStripoTemplate(html: string, options?: ConvertStripoOptions): ImportResult$1;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/detect.d.ts
|
|
19
|
+
type StripoKind = "editor" | "compiled";
|
|
20
|
+
/**
|
|
21
|
+
* Which Stripo HTML surface this is, or null if it is not Stripo markup.
|
|
22
|
+
*
|
|
23
|
+
* Editor-source (`getTemplateData`) carries `esd-*` on elements. Compiled
|
|
24
|
+
* export carries `es-wrapper` / `es-*-body` and never `esd-*` on a class
|
|
25
|
+
* attribute. A CSS-only leftover (`.esd-block-html` in a stylesheet) is not
|
|
26
|
+
* a discriminator — stylesheets are stripped before the check.
|
|
27
|
+
*/
|
|
28
|
+
export declare function detectStripoKind(html: string): StripoKind | null;
|
|
29
|
+
export declare function isStripoHtml(html: string): boolean;
|
|
30
|
+
//#endregion
|
|
31
|
+
export type { ConversionStatus, ConvertStripoOptions, ImportReport, ImportReportEntry, ImportResult, StripoKind };
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import { load } from "cheerio";
|
|
2
|
+
import { convertHtmlTemplate } from "@templatical/import-html";
|
|
3
|
+
import { createButtonBlock, createDefaultTemplateContent, createHtmlBlock, createMenuBlock, createSectionBlock, createSocialIconsBlock, createSpacerBlock, generateId } from "@templatical/types";
|
|
4
|
+
//#region src/classes.ts
|
|
5
|
+
function classTokens($el) {
|
|
6
|
+
return ($el.attr("class") ?? "").trim().split(/\s+/).filter(Boolean);
|
|
7
|
+
}
|
|
8
|
+
function hasToken($el, token) {
|
|
9
|
+
return classTokens($el).includes(token);
|
|
10
|
+
}
|
|
11
|
+
function hasAnyToken($el, tokens) {
|
|
12
|
+
const set = new Set(classTokens($el));
|
|
13
|
+
return tokens.some((t) => set.has(t));
|
|
14
|
+
}
|
|
15
|
+
function tokenStartingWith($el, prefix) {
|
|
16
|
+
return classTokens($el).find((t) => t.startsWith(prefix));
|
|
17
|
+
}
|
|
18
|
+
/** Elements carrying `token` that are not nested in another such element. */
|
|
19
|
+
function topLevelWithToken($, token, $root) {
|
|
20
|
+
const selector = `[class~="${token}"]`;
|
|
21
|
+
const $found = $root ? $root.find(selector) : $(selector);
|
|
22
|
+
const out = [];
|
|
23
|
+
$found.each((_, node) => {
|
|
24
|
+
const $el = $(node);
|
|
25
|
+
const nested = $el.parents(selector);
|
|
26
|
+
if ($root && $root.length) {
|
|
27
|
+
const outside = nested.filter((_, p) => {
|
|
28
|
+
const $p = $(p);
|
|
29
|
+
return $p.closest($root).length === 0 && !$p.is($root);
|
|
30
|
+
});
|
|
31
|
+
if (nested.length - outside.length > 0) return;
|
|
32
|
+
} else if (nested.length > 0) return;
|
|
33
|
+
out.push($el);
|
|
34
|
+
});
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function isHidden($el) {
|
|
38
|
+
return hasToken($el, "es-hidden") || hasToken($el, "esd-hidden");
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/css.ts
|
|
42
|
+
/**
|
|
43
|
+
* Tiny CSS readers for paint that compiled Stripo HTML already inlines.
|
|
44
|
+
* Hex (3/6) and a bare bgcolor hex; "transparent" is unset.
|
|
45
|
+
*/
|
|
46
|
+
function parseStyleAttribute(styleAttr) {
|
|
47
|
+
const result = {};
|
|
48
|
+
if (!styleAttr) return result;
|
|
49
|
+
for (const decl of styleAttr.split(";")) {
|
|
50
|
+
const idx = decl.indexOf(":");
|
|
51
|
+
if (idx === -1) continue;
|
|
52
|
+
const key = decl.slice(0, idx).trim().toLowerCase();
|
|
53
|
+
const value = decl.slice(idx + 1).trim();
|
|
54
|
+
if (key && value) result[key] = value;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function parseColor(value) {
|
|
59
|
+
if (!value) return "";
|
|
60
|
+
const trimmed = value.trim().toLowerCase();
|
|
61
|
+
if (trimmed === "transparent" || trimmed === "inherit" || trimmed === "none") return "";
|
|
62
|
+
const hex = trimmed.startsWith("#") ? trimmed : /^[0-9a-f]{3}$|^[0-9a-f]{6}$/.test(trimmed) ? `#${trimmed}` : trimmed;
|
|
63
|
+
if (/^#[0-9a-f]{6}$/.test(hex)) return hex;
|
|
64
|
+
if (/^#[0-9a-f]{3}$/.test(hex)) return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
function parsePxValue(value) {
|
|
68
|
+
if (!value) return 0;
|
|
69
|
+
const match = value.trim().match(/^(-?\d+(?:\.\d+)?)\s*(?:px)?\s*$/);
|
|
70
|
+
return match ? Math.round(parseFloat(match[1])) : 0;
|
|
71
|
+
}
|
|
72
|
+
/** Inline `background-color` / `background` wins over the `bgcolor` attribute. */
|
|
73
|
+
function colorFromPaint(styleAttr, bgcolor) {
|
|
74
|
+
const styles = parseStyleAttribute(styleAttr);
|
|
75
|
+
const fromShorthand = styles.background ? parseColor(styles.background.trim().split(/\s+/)[0]) : "";
|
|
76
|
+
return parseColor(styles["background-color"]) || fromShorthand || parseColor(bgcolor) || "";
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/fragment.ts
|
|
80
|
+
/** Paint after `createSectionBlock` so `styles` keeps its required padding. */
|
|
81
|
+
function withBackground(section, backgroundColor) {
|
|
82
|
+
if (backgroundColor) section.styles.backgroundColor = backgroundColor;
|
|
83
|
+
return section;
|
|
84
|
+
}
|
|
85
|
+
function flattenBlocks(blocks) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const b of blocks) if (b.type === "section") for (const col of b.children ?? []) out.push(...flattenBlocks(col));
|
|
88
|
+
else out.push(b);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** Run the generic HTML importer on a subtree and keep its blocks, not its sections. */
|
|
92
|
+
function blocksFromHtml(inner, ctx) {
|
|
93
|
+
const wrapped = `<!DOCTYPE html><html><body>${inner}</body></html>`;
|
|
94
|
+
const result = convertHtmlTemplate(wrapped);
|
|
95
|
+
ctx.entries.push(...result.report.entries);
|
|
96
|
+
ctx.warnings.push(...result.report.warnings);
|
|
97
|
+
return flattenBlocks(result.content.blocks);
|
|
98
|
+
}
|
|
99
|
+
function pushEntry(ctx, sourceTag, templaticalBlockType, status, note) {
|
|
100
|
+
ctx.entries.push({
|
|
101
|
+
sourceTag,
|
|
102
|
+
templaticalBlockType,
|
|
103
|
+
status,
|
|
104
|
+
...note ? { note } : {}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/platform.ts
|
|
109
|
+
const NAMED = {
|
|
110
|
+
facebook: "facebook",
|
|
111
|
+
fb: "facebook",
|
|
112
|
+
twitter: "twitter",
|
|
113
|
+
x: "twitter",
|
|
114
|
+
instagram: "instagram",
|
|
115
|
+
inst: "instagram",
|
|
116
|
+
linkedin: "linkedin",
|
|
117
|
+
youtube: "youtube",
|
|
118
|
+
yt: "youtube",
|
|
119
|
+
tiktok: "tiktok",
|
|
120
|
+
pinterest: "pinterest",
|
|
121
|
+
email: "email",
|
|
122
|
+
mail: "email",
|
|
123
|
+
whatsapp: "whatsapp",
|
|
124
|
+
telegram: "telegram",
|
|
125
|
+
discord: "discord",
|
|
126
|
+
snapchat: "snapchat",
|
|
127
|
+
reddit: "reddit",
|
|
128
|
+
github: "github",
|
|
129
|
+
dribbble: "dribbble",
|
|
130
|
+
behance: "behance"
|
|
131
|
+
};
|
|
132
|
+
function peel(raw) {
|
|
133
|
+
return raw.toLowerCase().replace(/\.(png|jpe?g|gif|svg|webp)$/i, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Map a Stripo icon's title, alt, or src stem onto a SocialPlatform.
|
|
137
|
+
* Unknown names become `website` — same floor as the MJML importer.
|
|
138
|
+
*/
|
|
139
|
+
function normalizePlatform(name, src, alt) {
|
|
140
|
+
for (const candidate of [
|
|
141
|
+
name,
|
|
142
|
+
alt,
|
|
143
|
+
src
|
|
144
|
+
]) {
|
|
145
|
+
if (!candidate) continue;
|
|
146
|
+
const words = peel(candidate).split(/\s+/);
|
|
147
|
+
for (const w of words) if (NAMED[w]) return NAMED[w];
|
|
148
|
+
const stem = peel(candidate.split("/").pop() ?? "");
|
|
149
|
+
for (const w of stem.split(/\s+/)) if (NAMED[w]) return NAMED[w];
|
|
150
|
+
}
|
|
151
|
+
return "website";
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/labelled.ts
|
|
155
|
+
function firstAnchor($el) {
|
|
156
|
+
return $el.is("a") ? $el : $el.find("a").first();
|
|
157
|
+
}
|
|
158
|
+
function buttonFrom($el, sourceTag, ctx) {
|
|
159
|
+
const $a = firstAnchor($el);
|
|
160
|
+
const text = ($a.text() ?? "").trim() || "Shop Now";
|
|
161
|
+
const url = $a.attr("href") ?? "#";
|
|
162
|
+
const target = $a.attr("target");
|
|
163
|
+
const own = parseStyleAttribute($a.attr("style"));
|
|
164
|
+
const parent = parseStyleAttribute($a.parent().attr("style"));
|
|
165
|
+
const backgroundColor = colorFromPaint($a.attr("style")) || colorFromPaint($a.parent().attr("style"));
|
|
166
|
+
const textColor = parseColor(own.color) || parseColor(parent.color);
|
|
167
|
+
const borderRadius = parsePxValue(own["border-radius"]) || parsePxValue(parent["border-radius"]);
|
|
168
|
+
pushEntry(ctx, sourceTag, "button", "converted");
|
|
169
|
+
return createButtonBlock({
|
|
170
|
+
text,
|
|
171
|
+
url,
|
|
172
|
+
...target === "_blank" ? { openInNewTab: true } : {},
|
|
173
|
+
...backgroundColor ? { backgroundColor } : {},
|
|
174
|
+
...textColor ? { textColor } : {},
|
|
175
|
+
...borderRadius > 0 ? { borderRadius } : {}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function spacerFrom($el, sourceTag, ctx) {
|
|
179
|
+
const style = $el.attr("style") ?? "";
|
|
180
|
+
const h = /(?:^|;)\s*height\s*:\s*(\d+)px/i.exec(style);
|
|
181
|
+
const attrH = $el.attr("height");
|
|
182
|
+
const height = h ? Number(h[1]) : attrH ? Number(attrH) : 24;
|
|
183
|
+
pushEntry(ctx, sourceTag, "spacer", "converted");
|
|
184
|
+
return createSpacerBlock({ height: Number.isFinite(height) && height > 0 ? height : 24 });
|
|
185
|
+
}
|
|
186
|
+
function menuItems($table, $) {
|
|
187
|
+
const items = [];
|
|
188
|
+
$table.find("td").each((_, td) => {
|
|
189
|
+
const $td = $(td);
|
|
190
|
+
if (isHidden($td)) return;
|
|
191
|
+
const $a = $td.find("a").first();
|
|
192
|
+
if (!$a.length) return;
|
|
193
|
+
const text = ($a.text() ?? "").trim();
|
|
194
|
+
if (!text) return;
|
|
195
|
+
items.push({
|
|
196
|
+
id: generateId(),
|
|
197
|
+
text,
|
|
198
|
+
url: $a.attr("href") ?? "#",
|
|
199
|
+
openInNewTab: $a.attr("target") === "_blank",
|
|
200
|
+
bold: false,
|
|
201
|
+
underline: false
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
return items;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* A compiled/editor `table.es-menu` / `esd-block-menu` is a MenuBlock only
|
|
208
|
+
* when one row holds two or more item cells. A single 100% cell is a stacked
|
|
209
|
+
* step (Password reset) and must fall through.
|
|
210
|
+
*/
|
|
211
|
+
function menuFrom($el, $, sourceTag, ctx) {
|
|
212
|
+
const $table = $el.is("table") ? $el : $el.find("table").first();
|
|
213
|
+
if ($table.find("tr").first().children("td").toArray().filter((td) => !isHidden($(td))).length < 2) return null;
|
|
214
|
+
const items = menuItems($table, $);
|
|
215
|
+
if (items.length < 2) return null;
|
|
216
|
+
pushEntry(ctx, sourceTag, "menu", "converted");
|
|
217
|
+
return createMenuBlock({ items });
|
|
218
|
+
}
|
|
219
|
+
function socialFrom($el, $, sourceTag, ctx) {
|
|
220
|
+
const icons = [];
|
|
221
|
+
$el.find("a").each((_, a) => {
|
|
222
|
+
const $a = $(a);
|
|
223
|
+
const $img = $a.find("img").first();
|
|
224
|
+
const url = $a.attr("href") ?? "#";
|
|
225
|
+
const platform = normalizePlatform($img.attr("title"), $img.attr("src"), $img.attr("alt"));
|
|
226
|
+
icons.push({
|
|
227
|
+
id: generateId(),
|
|
228
|
+
platform,
|
|
229
|
+
url
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
pushEntry(ctx, sourceTag, "social", "converted");
|
|
233
|
+
return createSocialIconsBlock({ icons });
|
|
234
|
+
}
|
|
235
|
+
function labelledInnerBlocks($el, $, ctx) {
|
|
236
|
+
return blocksFromHtml($.html($el) ?? "", ctx);
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/compiled.ts
|
|
240
|
+
function paintOf($el) {
|
|
241
|
+
return colorFromPaint($el.attr("style"), $el.attr("bgcolor"));
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The 600px band (`es-*-body`) carries the fill; the outer stripe is often
|
|
245
|
+
* `transparent`. Style wins over `bgcolor` (a white bgcolor with a mint
|
|
246
|
+
* `background-color` is the compiled password-reset shape).
|
|
247
|
+
*/
|
|
248
|
+
function stripeFill($stripe) {
|
|
249
|
+
const $body = $stripe.find("[class~='es-header-body'], [class~='es-content-body'], [class~='es-footer-body']").first();
|
|
250
|
+
if ($body.length) {
|
|
251
|
+
const fromBody = paintOf($body);
|
|
252
|
+
if (fromBody) return fromBody;
|
|
253
|
+
}
|
|
254
|
+
return paintOf($stripe);
|
|
255
|
+
}
|
|
256
|
+
const STRIPE_TOKENS = [
|
|
257
|
+
"es-header",
|
|
258
|
+
"es-content",
|
|
259
|
+
"es-footer"
|
|
260
|
+
];
|
|
261
|
+
function stripeToken($el) {
|
|
262
|
+
return STRIPE_TOKENS.find((t) => hasToken($el, t));
|
|
263
|
+
}
|
|
264
|
+
function topStripes($) {
|
|
265
|
+
const out = [];
|
|
266
|
+
$("[class]").each((_, node) => {
|
|
267
|
+
const $el = $(node);
|
|
268
|
+
if (!stripeToken($el)) return;
|
|
269
|
+
if ($el.parents().toArray().some((p) => hasAnyToken($(p), STRIPE_TOKENS))) return;
|
|
270
|
+
out.push($el);
|
|
271
|
+
});
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
function floatedColumns($stripe, $) {
|
|
275
|
+
const out = [];
|
|
276
|
+
$stripe.find("[class~='es-left'], [class~='es-right']").each((_, node) => {
|
|
277
|
+
const $el = $(node);
|
|
278
|
+
if (isHidden($el)) return;
|
|
279
|
+
if ($el.parents("[class~='es-left'], [class~='es-right']").filter((_, p) => {
|
|
280
|
+
return $stripe.find(p).length > 0 || $(p).closest($stripe).length > 0;
|
|
281
|
+
}).length > 0) return;
|
|
282
|
+
if ($el.parents().toArray().some((p) => {
|
|
283
|
+
if (p === $stripe[0]) return false;
|
|
284
|
+
return hasAnyToken($(p), STRIPE_TOKENS);
|
|
285
|
+
})) return;
|
|
286
|
+
out.push($el);
|
|
287
|
+
});
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
function columnLayout$1(count, ctx, sourceTag) {
|
|
291
|
+
if (count <= 1) return {
|
|
292
|
+
layout: "1",
|
|
293
|
+
flattenExtra: false
|
|
294
|
+
};
|
|
295
|
+
if (count === 2) return {
|
|
296
|
+
layout: "2",
|
|
297
|
+
flattenExtra: false
|
|
298
|
+
};
|
|
299
|
+
if (count === 3) return {
|
|
300
|
+
layout: "3",
|
|
301
|
+
flattenExtra: false
|
|
302
|
+
};
|
|
303
|
+
pushEntry(ctx, sourceTag, "section", "approximated", `Row with ${count} columns was flattened; extra columns merged into the third slot. Templatical supports up to 3 columns per section.`);
|
|
304
|
+
ctx.warnings.push(`Row with ${count} columns was flattened; extra columns merged into the third slot. Templatical supports up to 3 columns per section.`);
|
|
305
|
+
return {
|
|
306
|
+
layout: "3",
|
|
307
|
+
flattenExtra: true
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function convertSubtree($el, $, ctx) {
|
|
311
|
+
const $clone = $el.clone();
|
|
312
|
+
const replacements = [];
|
|
313
|
+
const take = ($node, block) => {
|
|
314
|
+
if (!block) return;
|
|
315
|
+
const marker = `stripo-keep-${replacements.length}`;
|
|
316
|
+
replacements.push([block]);
|
|
317
|
+
$node.replaceWith(`<div data-stripo-keep="${marker}"></div>`);
|
|
318
|
+
};
|
|
319
|
+
$clone.find("table.es-menu, [class~='es-menu']").each((_, node) => {
|
|
320
|
+
const $n = $(node);
|
|
321
|
+
if (!$n.is("table") && $n.find("table").length) return;
|
|
322
|
+
const menu = menuFrom($n, $, "es-menu", ctx);
|
|
323
|
+
take($n, menu);
|
|
324
|
+
});
|
|
325
|
+
$clone.find("[class~='es-social']").each((_, node) => {
|
|
326
|
+
const $n = $(node);
|
|
327
|
+
take($n, socialFrom($n, $, "es-social", ctx));
|
|
328
|
+
});
|
|
329
|
+
$clone.find("a.es-button, [class~='es-button']").each((_, node) => {
|
|
330
|
+
const $n = $(node);
|
|
331
|
+
if (!$n.is("a") && $n.find("a").length === 0) return;
|
|
332
|
+
take($n, buttonFrom($n, "es-button", ctx));
|
|
333
|
+
});
|
|
334
|
+
$clone.find("[class~='es-spacer']").each((_, node) => {
|
|
335
|
+
const $n = $(node);
|
|
336
|
+
take($n, spacerFrom($n, "es-spacer", ctx));
|
|
337
|
+
});
|
|
338
|
+
const generic = blocksFromHtml($.html($clone) ?? "", ctx);
|
|
339
|
+
const out = [];
|
|
340
|
+
const used = /* @__PURE__ */ new Set();
|
|
341
|
+
const walk = (blocks) => {
|
|
342
|
+
for (const b of blocks) {
|
|
343
|
+
if (b.type === "html" && /data-stripo-keep="stripo-keep-(\d+)"/.test(b.content)) {
|
|
344
|
+
const m = /data-stripo-keep="stripo-keep-(\d+)"/.exec(b.content);
|
|
345
|
+
const i = m ? Number(m[1]) : -1;
|
|
346
|
+
if (i >= 0 && replacements[i] && !used.has(i)) {
|
|
347
|
+
used.add(i);
|
|
348
|
+
out.push(...replacements[i]);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (b.type === "paragraph" && /stripo-keep-/.test(b.content ?? "")) {
|
|
353
|
+
const m = /stripo-keep-(\d+)/.exec(b.content);
|
|
354
|
+
const i = m ? Number(m[1]) : -1;
|
|
355
|
+
if (i >= 0 && replacements[i] && !used.has(i)) {
|
|
356
|
+
used.add(i);
|
|
357
|
+
out.push(...replacements[i]);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
out.push(b);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
walk(generic);
|
|
365
|
+
for (let i = 0; i < replacements.length; i++) if (!used.has(i)) out.push(...replacements[i]);
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
function convertCompiled(html, ctx) {
|
|
369
|
+
const $ = load(html);
|
|
370
|
+
const stripes = topStripes($);
|
|
371
|
+
if (stripes.length === 0) return convertSubtree($.root(), $, ctx);
|
|
372
|
+
return stripes.map(($stripe) => {
|
|
373
|
+
const token = stripeToken($stripe) ?? "es-content";
|
|
374
|
+
const cols = floatedColumns($stripe, $);
|
|
375
|
+
const { layout, flattenExtra } = columnLayout$1(cols.length || 1, ctx, token);
|
|
376
|
+
let children;
|
|
377
|
+
if (cols.length <= 1) children = [convertSubtree($stripe, $, ctx)];
|
|
378
|
+
else if (flattenExtra) {
|
|
379
|
+
const slots = cols.slice(0, 2).map(($c) => convertSubtree($c, $, ctx));
|
|
380
|
+
const rest = [];
|
|
381
|
+
for (const $c of cols.slice(2)) rest.push(...convertSubtree($c, $, ctx));
|
|
382
|
+
slots.push(rest);
|
|
383
|
+
children = slots;
|
|
384
|
+
} else children = cols.map(($c) => convertSubtree($c, $, ctx));
|
|
385
|
+
if (!flattenExtra) pushEntry(ctx, token, "section", "converted");
|
|
386
|
+
return withBackground(createSectionBlock({
|
|
387
|
+
columns: layout,
|
|
388
|
+
children
|
|
389
|
+
}), stripeFill($stripe));
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/detect.ts
|
|
394
|
+
const EDITOR_TOKENS = /* @__PURE__ */ new Set([
|
|
395
|
+
"esd-stripe",
|
|
396
|
+
"esd-structure",
|
|
397
|
+
"esd-container-frame"
|
|
398
|
+
]);
|
|
399
|
+
const COMPILED_TOKENS = /* @__PURE__ */ new Set([
|
|
400
|
+
"es-wrapper",
|
|
401
|
+
"es-content-body",
|
|
402
|
+
"es-header-body"
|
|
403
|
+
]);
|
|
404
|
+
function isTagNameEnd(ch) {
|
|
405
|
+
return ch === void 0 || !/[A-Za-z0-9_]/.test(ch);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Drop every `tag` element, including a closer with extra attributes or
|
|
409
|
+
* whitespace (`</style >`, `<\/script foo>`). Linear `indexOf` — a regex
|
|
410
|
+
* here fails `js/bad-tag-filter` or `js/polynomial-redos`, and Cheerio
|
|
411
|
+
* rewrites a bare `<td>` fragment and would drop the attributes we need.
|
|
412
|
+
*/
|
|
413
|
+
function withoutElements(html, tag) {
|
|
414
|
+
const open = `<${tag}`;
|
|
415
|
+
const close = `</${tag}`;
|
|
416
|
+
const lower = html.toLowerCase();
|
|
417
|
+
let out = "";
|
|
418
|
+
let pos = 0;
|
|
419
|
+
while (pos < html.length) {
|
|
420
|
+
const start = lower.indexOf(open, pos);
|
|
421
|
+
if (start === -1) {
|
|
422
|
+
out += html.slice(pos);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
if (!isTagNameEnd(lower[start + open.length])) {
|
|
426
|
+
out += html.slice(pos, start + open.length);
|
|
427
|
+
pos = start + open.length;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
out += html.slice(pos, start);
|
|
431
|
+
const gt = html.indexOf(">", start);
|
|
432
|
+
if (gt === -1) break;
|
|
433
|
+
const closeAt = lower.indexOf(close, gt + 1);
|
|
434
|
+
if (closeAt === -1) break;
|
|
435
|
+
const closeGt = html.indexOf(">", closeAt);
|
|
436
|
+
if (closeGt === -1) break;
|
|
437
|
+
out += " ";
|
|
438
|
+
pos = closeGt + 1;
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Class tokens from attributes only. Stylesheets and scripts are dropped
|
|
444
|
+
* first so a compiled leftover `.esd-block-html table` rule cannot trip
|
|
445
|
+
* editor detection.
|
|
446
|
+
*/
|
|
447
|
+
function markupClassTokens(html) {
|
|
448
|
+
const stripped = withoutElements(withoutElements(html, "style"), "script");
|
|
449
|
+
const tokens = [];
|
|
450
|
+
const re = /\bclass\s*=\s*(["'])([^"']*)\1/gi;
|
|
451
|
+
let m;
|
|
452
|
+
while (m = re.exec(stripped)) tokens.push(...m[2].trim().split(/\s+/).filter(Boolean));
|
|
453
|
+
return tokens;
|
|
454
|
+
}
|
|
455
|
+
function isEditorToken(token) {
|
|
456
|
+
return EDITOR_TOKENS.has(token) || token.startsWith("esd-block-");
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Which Stripo HTML surface this is, or null if it is not Stripo markup.
|
|
460
|
+
*
|
|
461
|
+
* Editor-source (`getTemplateData`) carries `esd-*` on elements. Compiled
|
|
462
|
+
* export carries `es-wrapper` / `es-*-body` and never `esd-*` on a class
|
|
463
|
+
* attribute. A CSS-only leftover (`.esd-block-html` in a stylesheet) is not
|
|
464
|
+
* a discriminator — stylesheets are stripped before the check.
|
|
465
|
+
*/
|
|
466
|
+
function detectStripoKind(html) {
|
|
467
|
+
if (typeof html !== "string" || html.trim().length === 0) return null;
|
|
468
|
+
const tokens = markupClassTokens(html);
|
|
469
|
+
if (tokens.some(isEditorToken)) return "editor";
|
|
470
|
+
if (tokens.some((t) => COMPILED_TOKENS.has(t))) return "compiled";
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
function isStripoHtml(html) {
|
|
474
|
+
return detectStripoKind(html) !== null;
|
|
475
|
+
}
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/editor.ts
|
|
478
|
+
function columnLayout(count, ctx) {
|
|
479
|
+
if (count <= 1) return "1";
|
|
480
|
+
if (count === 2) return "2";
|
|
481
|
+
if (count === 3) return "3";
|
|
482
|
+
pushEntry(ctx, "esd-structure", "section", "approximated", `Row with ${count} columns was flattened to a single column. Templatical supports up to 3 columns per section.`);
|
|
483
|
+
ctx.warnings.push(`Row with ${count} columns was flattened to a single column. Templatical supports up to 3 columns per section.`);
|
|
484
|
+
return "1";
|
|
485
|
+
}
|
|
486
|
+
function esdBlockKind($el) {
|
|
487
|
+
const token = tokenStartingWith($el, "esd-block-");
|
|
488
|
+
return token ? token.slice(10) : void 0;
|
|
489
|
+
}
|
|
490
|
+
function convertEsdBlock($el, $, ctx) {
|
|
491
|
+
const kind = esdBlockKind($el);
|
|
492
|
+
if (!kind) return labelledInnerBlocks($el, $, ctx);
|
|
493
|
+
if (kind === "button") return [buttonFrom($el, "esd-block-button", ctx)];
|
|
494
|
+
if (kind === "spacer") return [spacerFrom($el, "esd-block-spacer", ctx)];
|
|
495
|
+
if (kind === "social") return [socialFrom($el, $, "esd-block-social", ctx)];
|
|
496
|
+
if (kind === "menu") {
|
|
497
|
+
const menu = menuFrom($el, $, "esd-block-menu", ctx);
|
|
498
|
+
if (menu) return [menu];
|
|
499
|
+
return labelledInnerBlocks($el, $, ctx);
|
|
500
|
+
}
|
|
501
|
+
if (kind === "html") {
|
|
502
|
+
pushEntry(ctx, "esd-block-html", "html", "html-fallback", "HTML block preserved as HTML.");
|
|
503
|
+
return [createHtmlBlock({ content: ($el.html() ?? "").trim() })];
|
|
504
|
+
}
|
|
505
|
+
pushEntry(ctx, `esd-block-${kind}`, kind === "image" ? "image" : "paragraph", "converted");
|
|
506
|
+
return labelledInnerBlocks($el, $, ctx);
|
|
507
|
+
}
|
|
508
|
+
function blocksInContainer($frame, $, ctx) {
|
|
509
|
+
const out = [];
|
|
510
|
+
const seen = /* @__PURE__ */ new Set();
|
|
511
|
+
$frame.find("[class]").each((_, node) => {
|
|
512
|
+
const $el = $(node);
|
|
513
|
+
if (!tokenStartingWith($el, "esd-block-")) return;
|
|
514
|
+
if ($el.parents().toArray().some((p) => tokenStartingWith($(p), "esd-block-"))) return;
|
|
515
|
+
if (seen.has(node)) return;
|
|
516
|
+
seen.add(node);
|
|
517
|
+
out.push(...convertEsdBlock($el, $, ctx));
|
|
518
|
+
});
|
|
519
|
+
if (out.length === 0) return labelledInnerBlocks($frame, $, ctx);
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
function sectionFromStructure($structure, $, ctx) {
|
|
523
|
+
const frames = $structure.find("[class~='esd-container-frame']").toArray().map((n) => $(n)).filter(($el) => {
|
|
524
|
+
if (isHidden($el)) return false;
|
|
525
|
+
return $el.parents("[class~='esd-container-frame']").filter((_, p) => {
|
|
526
|
+
return $(p).closest($structure).length > 0;
|
|
527
|
+
}).length === 0;
|
|
528
|
+
});
|
|
529
|
+
const layout = columnLayout(frames.length || 1, ctx);
|
|
530
|
+
let children;
|
|
531
|
+
if (layout === "1") {
|
|
532
|
+
const merged = [];
|
|
533
|
+
if (frames.length === 0) merged.push(...blocksInContainer($structure, $, ctx));
|
|
534
|
+
else for (const $f of frames) merged.push(...blocksInContainer($f, $, ctx));
|
|
535
|
+
children = [merged];
|
|
536
|
+
} else children = frames.map(($f) => blocksInContainer($f, $, ctx));
|
|
537
|
+
if (layout !== "1" || frames.length <= 3) pushEntry(ctx, "esd-structure", "section", "converted");
|
|
538
|
+
return withBackground(createSectionBlock({
|
|
539
|
+
columns: layout,
|
|
540
|
+
children
|
|
541
|
+
}), colorFromPaint($structure.attr("style"), $structure.attr("bgcolor")));
|
|
542
|
+
}
|
|
543
|
+
function convertEditor(html, ctx) {
|
|
544
|
+
const $ = load(html);
|
|
545
|
+
const structures = topLevelWithToken($, "esd-structure");
|
|
546
|
+
if (structures.length > 0) return structures.map(($s) => sectionFromStructure($s, $, ctx));
|
|
547
|
+
const stripes = topLevelWithToken($, "esd-stripe");
|
|
548
|
+
if (stripes.length === 0) return labelledInnerBlocks($.root(), $, ctx);
|
|
549
|
+
return stripes.map(($stripe) => {
|
|
550
|
+
const inner = $stripe.find("[class~='esd-structure']");
|
|
551
|
+
if (inner.length > 0) return sectionFromStructure(inner.first(), $, ctx);
|
|
552
|
+
pushEntry(ctx, "esd-stripe", "section", "converted");
|
|
553
|
+
return withBackground(createSectionBlock({
|
|
554
|
+
columns: "1",
|
|
555
|
+
children: [blocksInContainer($stripe, $, ctx)]
|
|
556
|
+
}), colorFromPaint($stripe.attr("style"), $stripe.attr("bgcolor")));
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/converter.ts
|
|
561
|
+
function injectCss(html, css) {
|
|
562
|
+
if (!css || css.trim().length === 0) return html;
|
|
563
|
+
const $ = load(html);
|
|
564
|
+
if ($("head").length === 0) $("html").prepend("<head></head>");
|
|
565
|
+
const safe = css.replace(/<\/style/gi, "<\\/style");
|
|
566
|
+
const $style = $("<style></style>").attr("data-stripo-css", "1").text(safe);
|
|
567
|
+
$("head").append($style);
|
|
568
|
+
return $.html() ?? html;
|
|
569
|
+
}
|
|
570
|
+
function summarize(ctx) {
|
|
571
|
+
const summary = {
|
|
572
|
+
total: ctx.entries.length,
|
|
573
|
+
converted: 0,
|
|
574
|
+
approximated: 0,
|
|
575
|
+
htmlFallback: 0,
|
|
576
|
+
skipped: 0
|
|
577
|
+
};
|
|
578
|
+
for (const e of ctx.entries) if (e.status === "converted") summary.converted++;
|
|
579
|
+
else if (e.status === "approximated") summary.approximated++;
|
|
580
|
+
else if (e.status === "html-fallback") summary.htmlFallback++;
|
|
581
|
+
else if (e.status === "skipped") summary.skipped++;
|
|
582
|
+
return {
|
|
583
|
+
entries: ctx.entries,
|
|
584
|
+
warnings: ctx.warnings,
|
|
585
|
+
summary
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Convert a Stripo template to Templatical content.
|
|
590
|
+
*
|
|
591
|
+
* Auto-detects the surface from class attributes:
|
|
592
|
+
* - `esd-*` on elements → plugin/editor `getTemplateData` HTML
|
|
593
|
+
* - `es-wrapper` / `es-*-body` without `esd-*` → compiled File→HTML export
|
|
594
|
+
*
|
|
595
|
+
* Pass `options.css` when converting plugin storage (`getTemplateData().css`).
|
|
596
|
+
*/
|
|
597
|
+
function convertStripoTemplate(html, options) {
|
|
598
|
+
if (typeof html !== "string") throw new Error("Invalid Stripo template: expected a string. Pass the HTML source as a string.");
|
|
599
|
+
if (html.trim().length === 0) throw new Error("Invalid Stripo template: input is empty. Pass the HTML source of a Stripo email.");
|
|
600
|
+
const prepared = injectCss(html, options?.css);
|
|
601
|
+
const kind = detectStripoKind(prepared);
|
|
602
|
+
const fallback = convertHtmlTemplate(prepared);
|
|
603
|
+
if (!kind) return {
|
|
604
|
+
content: fallback.content,
|
|
605
|
+
report: {
|
|
606
|
+
...fallback.report,
|
|
607
|
+
warnings: ["No Stripo class attributes found; converted as generic HTML.", ...fallback.report.warnings]
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
const ctx = {
|
|
611
|
+
entries: [],
|
|
612
|
+
warnings: []
|
|
613
|
+
};
|
|
614
|
+
const blocks = kind === "editor" ? convertEditor(prepared, ctx) : convertCompiled(prepared, ctx);
|
|
615
|
+
return {
|
|
616
|
+
content: {
|
|
617
|
+
...createDefaultTemplateContent(),
|
|
618
|
+
...fallback.content,
|
|
619
|
+
blocks
|
|
620
|
+
},
|
|
621
|
+
report: summarize(ctx)
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
//#endregion
|
|
625
|
+
export { convertStripoTemplate, detectStripoKind, isStripoHtml };
|
|
626
|
+
|
|
627
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["columnLayout"],"sources":["../src/classes.ts","../src/css.ts","../src/fragment.ts","../src/platform.ts","../src/labelled.ts","../src/compiled.ts","../src/detect.ts","../src/editor.ts","../src/converter.ts"],"sourcesContent":["import type { Cheerio, CheerioAPI } from \"cheerio\";\nimport type { Element } from \"domhandler\";\n\nexport function classTokens($el: Cheerio<Element>): string[] {\n return ($el.attr(\"class\") ?? \"\").trim().split(/\\s+/).filter(Boolean);\n}\n\nexport function hasToken($el: Cheerio<Element>, token: string): boolean {\n return classTokens($el).includes(token);\n}\n\nexport function hasAnyToken(\n $el: Cheerio<Element>,\n tokens: readonly string[],\n): boolean {\n const set = new Set(classTokens($el));\n return tokens.some((t) => set.has(t));\n}\n\nexport function tokenStartingWith(\n $el: Cheerio<Element>,\n prefix: string,\n): string | undefined {\n return classTokens($el).find((t) => t.startsWith(prefix));\n}\n\n/** Elements carrying `token` that are not nested in another such element. */\nexport function topLevelWithToken(\n $: CheerioAPI,\n token: string,\n $root?: Cheerio<Element>,\n): Cheerio<Element>[] {\n const selector = `[class~=\"${token}\"]`;\n const $found = $root ? $root.find(selector) : $(selector);\n const out: Cheerio<Element>[] = [];\n $found.each((_, node) => {\n const $el = $(node) as Cheerio<Element>;\n const nested = $el.parents(selector);\n if ($root && $root.length) {\n const outside = nested.filter((_, p) => {\n const $p = $(p) as Cheerio<Element>;\n return $p.closest($root).length === 0 && !$p.is($root);\n });\n if (nested.length - outside.length > 0) return;\n } else if (nested.length > 0) {\n return;\n }\n out.push($el);\n });\n return out;\n}\n\nexport function isHidden($el: Cheerio<Element>): boolean {\n return hasToken($el, \"es-hidden\") || hasToken($el, \"esd-hidden\");\n}\n","/**\n * Tiny CSS readers for paint that compiled Stripo HTML already inlines.\n * Hex (3/6) and a bare bgcolor hex; \"transparent\" is unset.\n */\n\nexport function parseStyleAttribute(\n styleAttr: string | undefined,\n): Record<string, string> {\n const result: Record<string, string> = {};\n if (!styleAttr) return result;\n for (const decl of styleAttr.split(\";\")) {\n const idx = decl.indexOf(\":\");\n if (idx === -1) continue;\n const key = decl.slice(0, idx).trim().toLowerCase();\n const value = decl.slice(idx + 1).trim();\n if (key && value) result[key] = value;\n }\n return result;\n}\n\nexport function parseColor(value: string | undefined): string {\n if (!value) return \"\";\n const trimmed = value.trim().toLowerCase();\n if (trimmed === \"transparent\" || trimmed === \"inherit\" || trimmed === \"none\")\n return \"\";\n const hex = trimmed.startsWith(\"#\")\n ? trimmed\n : /^[0-9a-f]{3}$|^[0-9a-f]{6}$/.test(trimmed)\n ? `#${trimmed}`\n : trimmed;\n if (/^#[0-9a-f]{6}$/.test(hex)) return hex;\n if (/^#[0-9a-f]{3}$/.test(hex)) {\n return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;\n }\n return \"\";\n}\n\nexport function parsePxValue(value: string | undefined): number {\n if (!value) return 0;\n const match = value.trim().match(/^(-?\\d+(?:\\.\\d+)?)\\s*(?:px)?\\s*$/);\n return match ? Math.round(parseFloat(match[1])) : 0;\n}\n\n/** Inline `background-color` / `background` wins over the `bgcolor` attribute. */\nexport function colorFromPaint(\n styleAttr: string | undefined,\n bgcolor?: string,\n): string {\n const styles = parseStyleAttribute(styleAttr);\n const fromShorthand = styles.background\n ? parseColor(styles.background.trim().split(/\\s+/)[0])\n : \"\";\n return (\n parseColor(styles[\"background-color\"]) ||\n fromShorthand ||\n parseColor(bgcolor) ||\n \"\"\n );\n}\n","import { convertHtmlTemplate } from \"@templatical/import-html\";\nimport type { ImportReportEntry } from \"@templatical/import-html\";\nimport type { Block, SectionBlock } from \"@templatical/types\";\n\nexport interface ConvertCtx {\n entries: ImportReportEntry[];\n warnings: string[];\n}\n\n/** Paint after `createSectionBlock` so `styles` keeps its required padding. */\nexport function withBackground(\n section: SectionBlock,\n backgroundColor: string,\n): SectionBlock {\n if (backgroundColor) section.styles.backgroundColor = backgroundColor;\n return section;\n}\n\nexport function flattenBlocks(blocks: Block[]): Block[] {\n const out: Block[] = [];\n for (const b of blocks) {\n if (b.type === \"section\") {\n for (const col of b.children ?? []) out.push(...flattenBlocks(col));\n } else {\n out.push(b);\n }\n }\n return out;\n}\n\n/** Run the generic HTML importer on a subtree and keep its blocks, not its sections. */\nexport function blocksFromHtml(inner: string, ctx: ConvertCtx): Block[] {\n const wrapped = `<!DOCTYPE html><html><body>${inner}</body></html>`;\n const result = convertHtmlTemplate(wrapped);\n ctx.entries.push(...result.report.entries);\n ctx.warnings.push(...result.report.warnings);\n return flattenBlocks(result.content.blocks);\n}\n\nexport function pushEntry(\n ctx: ConvertCtx,\n sourceTag: string,\n templaticalBlockType: string | null,\n status: ImportReportEntry[\"status\"],\n note?: string,\n): void {\n ctx.entries.push({\n sourceTag,\n templaticalBlockType,\n status,\n ...(note ? { note } : {}),\n });\n}\n","import type { SocialPlatform } from \"@templatical/types\";\n\nconst NAMED: Record<string, SocialPlatform> = {\n facebook: \"facebook\",\n fb: \"facebook\",\n twitter: \"twitter\",\n x: \"twitter\",\n instagram: \"instagram\",\n inst: \"instagram\",\n linkedin: \"linkedin\",\n youtube: \"youtube\",\n yt: \"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\nfunction peel(raw: string): string {\n return raw\n .toLowerCase()\n .replace(/\\.(png|jpe?g|gif|svg|webp)$/i, \"\")\n .replace(/[^a-z0-9]+/g, \" \")\n .trim();\n}\n\n/**\n * Map a Stripo icon's title, alt, or src stem onto a SocialPlatform.\n * Unknown names become `website` — same floor as the MJML importer.\n */\nexport function normalizePlatform(\n name?: string,\n src?: string,\n alt?: string,\n): SocialPlatform {\n for (const candidate of [name, alt, src]) {\n if (!candidate) continue;\n const words = peel(candidate).split(/\\s+/);\n for (const w of words) {\n if (NAMED[w]) return NAMED[w];\n }\n const stem = peel(candidate.split(\"/\").pop() ?? \"\");\n for (const w of stem.split(/\\s+/)) {\n if (NAMED[w]) return NAMED[w];\n }\n }\n return \"website\";\n}\n","import type { Cheerio, CheerioAPI } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport {\n createButtonBlock,\n createMenuBlock,\n createSocialIconsBlock,\n createSpacerBlock,\n generateId,\n} from \"@templatical/types\";\nimport type { Block, MenuItemData, SocialIcon } from \"@templatical/types\";\nimport { isHidden } from \"./classes\";\nimport {\n colorFromPaint,\n parseColor,\n parsePxValue,\n parseStyleAttribute,\n} from \"./css\";\nimport { blocksFromHtml, pushEntry, type ConvertCtx } from \"./fragment\";\nimport { normalizePlatform } from \"./platform\";\n\nfunction firstAnchor($el: Cheerio<Element>): Cheerio<Element> {\n const $self = $el.is(\"a\") ? $el : $el.find(\"a\").first();\n return $self as Cheerio<Element>;\n}\n\nexport function buttonFrom(\n $el: Cheerio<Element>,\n sourceTag: string,\n ctx: ConvertCtx,\n): Block {\n const $a = firstAnchor($el);\n const text = ($a.text() ?? \"\").trim() || \"Shop Now\";\n const url = $a.attr(\"href\") ?? \"#\";\n const target = $a.attr(\"target\");\n const own = parseStyleAttribute($a.attr(\"style\"));\n const parent = parseStyleAttribute($a.parent().attr(\"style\"));\n const backgroundColor =\n colorFromPaint($a.attr(\"style\")) ||\n colorFromPaint($a.parent().attr(\"style\"));\n const textColor = parseColor(own.color) || parseColor(parent.color);\n const borderRadius =\n parsePxValue(own[\"border-radius\"]) || parsePxValue(parent[\"border-radius\"]);\n pushEntry(ctx, sourceTag, \"button\", \"converted\");\n return createButtonBlock({\n text,\n url,\n ...(target === \"_blank\" ? { openInNewTab: true } : {}),\n ...(backgroundColor ? { backgroundColor } : {}),\n ...(textColor ? { textColor } : {}),\n ...(borderRadius > 0 ? { borderRadius } : {}),\n });\n}\n\nexport function spacerFrom(\n $el: Cheerio<Element>,\n sourceTag: string,\n ctx: ConvertCtx,\n): Block {\n const style = $el.attr(\"style\") ?? \"\";\n const h = /(?:^|;)\\s*height\\s*:\\s*(\\d+)px/i.exec(style);\n const attrH = $el.attr(\"height\");\n const height = h ? Number(h[1]) : attrH ? Number(attrH) : 24;\n pushEntry(ctx, sourceTag, \"spacer\", \"converted\");\n return createSpacerBlock({\n height: Number.isFinite(height) && height > 0 ? height : 24,\n });\n}\n\nfunction menuItems($table: Cheerio<Element>, $: CheerioAPI): MenuItemData[] {\n const items: MenuItemData[] = [];\n $table.find(\"td\").each((_, td) => {\n const $td = $(td) as Cheerio<Element>;\n if (isHidden($td)) return;\n const $a = $td.find(\"a\").first();\n if (!$a.length) return;\n const text = ($a.text() ?? \"\").trim();\n if (!text) return;\n items.push({\n id: generateId(),\n text,\n url: $a.attr(\"href\") ?? \"#\",\n openInNewTab: $a.attr(\"target\") === \"_blank\",\n bold: false,\n underline: false,\n });\n });\n return items;\n}\n\n/**\n * A compiled/editor `table.es-menu` / `esd-block-menu` is a MenuBlock only\n * when one row holds two or more item cells. A single 100% cell is a stacked\n * step (Password reset) and must fall through.\n */\nexport function menuFrom(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n sourceTag: string,\n ctx: ConvertCtx,\n): Block | null {\n const $table = $el.is(\"table\") ? $el : $el.find(\"table\").first();\n const $row = $table.find(\"tr\").first();\n const itemCells = $row\n .children(\"td\")\n .toArray()\n .filter((td) => !isHidden($(td) as Cheerio<Element>));\n if (itemCells.length < 2) return null;\n const items = menuItems($table, $);\n if (items.length < 2) return null;\n pushEntry(ctx, sourceTag, \"menu\", \"converted\");\n return createMenuBlock({ items });\n}\n\nexport function socialFrom(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n sourceTag: string,\n ctx: ConvertCtx,\n): Block {\n const icons: SocialIcon[] = [];\n $el.find(\"a\").each((_, a) => {\n const $a = $(a) as Cheerio<Element>;\n const $img = $a.find(\"img\").first();\n const url = $a.attr(\"href\") ?? \"#\";\n const platform = normalizePlatform(\n $img.attr(\"title\"),\n $img.attr(\"src\"),\n $img.attr(\"alt\"),\n );\n icons.push({ id: generateId(), platform, url });\n });\n pushEntry(ctx, sourceTag, \"social\", \"converted\");\n return createSocialIconsBlock({ icons });\n}\n\nexport function labelledInnerBlocks(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n ctx: ConvertCtx,\n): Block[] {\n return blocksFromHtml($.html($el) ?? \"\", ctx);\n}\n","import { load, type Cheerio, type CheerioAPI } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport { createSectionBlock } from \"@templatical/types\";\nimport type { Block, ColumnLayout } from \"@templatical/types\";\nimport { hasAnyToken, hasToken, isHidden } from \"./classes\";\nimport { colorFromPaint } from \"./css\";\nimport {\n blocksFromHtml,\n pushEntry,\n withBackground,\n type ConvertCtx,\n} from \"./fragment\";\nimport { buttonFrom, menuFrom, socialFrom, spacerFrom } from \"./labelled\";\n\nfunction paintOf($el: Cheerio<Element>): string {\n return colorFromPaint($el.attr(\"style\"), $el.attr(\"bgcolor\"));\n}\n\n/**\n * The 600px band (`es-*-body`) carries the fill; the outer stripe is often\n * `transparent`. Style wins over `bgcolor` (a white bgcolor with a mint\n * `background-color` is the compiled password-reset shape).\n */\nfunction stripeFill($stripe: Cheerio<Element>): string {\n const $body = $stripe\n .find(\n \"[class~='es-header-body'], [class~='es-content-body'], [class~='es-footer-body']\",\n )\n .first();\n if ($body.length) {\n const fromBody = paintOf($body as Cheerio<Element>);\n if (fromBody) return fromBody;\n }\n return paintOf($stripe);\n}\n\nconst STRIPE_TOKENS = [\"es-header\", \"es-content\", \"es-footer\"] as const;\n\nfunction stripeToken($el: Cheerio<Element>): string | undefined {\n return STRIPE_TOKENS.find((t) => hasToken($el, t));\n}\n\nfunction topStripes($: CheerioAPI): Cheerio<Element>[] {\n const out: Cheerio<Element>[] = [];\n $(\"[class]\").each((_, node) => {\n const $el = $(node) as Cheerio<Element>;\n const token = stripeToken($el);\n if (!token) return;\n const nested = $el\n .parents()\n .toArray()\n .some((p) => hasAnyToken($(p) as Cheerio<Element>, STRIPE_TOKENS));\n if (nested) return;\n out.push($el);\n });\n return out;\n}\n\nfunction floatedColumns(\n $stripe: Cheerio<Element>,\n $: CheerioAPI,\n): Cheerio<Element>[] {\n const out: Cheerio<Element>[] = [];\n $stripe.find(\"[class~='es-left'], [class~='es-right']\").each((_, node) => {\n const $el = $(node) as Cheerio<Element>;\n if (isHidden($el)) return;\n const nested = $el\n .parents(\"[class~='es-left'], [class~='es-right']\")\n .filter((_, p) => {\n return $stripe.find(p).length > 0 || $(p).closest($stripe).length > 0;\n });\n if (nested.length > 0) return;\n const inOtherStripe = $el\n .parents()\n .toArray()\n .some((p) => {\n if (p === $stripe[0]) return false;\n return hasAnyToken($(p) as Cheerio<Element>, STRIPE_TOKENS);\n });\n if (inOtherStripe) return;\n out.push($el);\n });\n return out;\n}\n\nfunction columnLayout(\n count: number,\n ctx: ConvertCtx,\n sourceTag: string,\n): {\n layout: ColumnLayout;\n flattenExtra: boolean;\n} {\n if (count <= 1) return { layout: \"1\", flattenExtra: false };\n if (count === 2) return { layout: \"2\", flattenExtra: false };\n if (count === 3) return { layout: \"3\", flattenExtra: false };\n pushEntry(\n ctx,\n sourceTag,\n \"section\",\n \"approximated\",\n `Row with ${count} columns was flattened; extra columns merged into the third slot. Templatical supports up to 3 columns per section.`,\n );\n ctx.warnings.push(\n `Row with ${count} columns was flattened; extra columns merged into the third slot. Templatical supports up to 3 columns per section.`,\n );\n return { layout: \"3\", flattenExtra: true };\n}\n\nfunction convertSubtree(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n ctx: ConvertCtx,\n): Block[] {\n const $clone = $el.clone();\n const replacements: Block[][] = [];\n\n const take = ($node: Cheerio<Element>, block: Block | null) => {\n if (!block) return;\n const marker = `stripo-keep-${replacements.length}`;\n replacements.push([block]);\n $node.replaceWith(`<div data-stripo-keep=\"${marker}\"></div>`);\n };\n\n $clone.find(\"table.es-menu, [class~='es-menu']\").each((_, node) => {\n const $n = $(node) as Cheerio<Element>;\n if (!$n.is(\"table\") && $n.find(\"table\").length) return;\n const menu = menuFrom($n, $, \"es-menu\", ctx);\n take($n, menu);\n });\n $clone.find(\"[class~='es-social']\").each((_, node) => {\n const $n = $(node) as Cheerio<Element>;\n take($n, socialFrom($n, $, \"es-social\", ctx));\n });\n $clone.find(\"a.es-button, [class~='es-button']\").each((_, node) => {\n const $n = $(node) as Cheerio<Element>;\n if (!$n.is(\"a\") && $n.find(\"a\").length === 0) return;\n take($n, buttonFrom($n, \"es-button\", ctx));\n });\n $clone.find(\"[class~='es-spacer']\").each((_, node) => {\n const $n = $(node) as Cheerio<Element>;\n take($n, spacerFrom($n, \"es-spacer\", ctx));\n });\n\n const html = $.html($clone) ?? \"\";\n const generic = blocksFromHtml(html, ctx);\n const out: Block[] = [];\n const used = new Set<number>();\n const walk = (blocks: Block[]) => {\n for (const b of blocks) {\n if (\n b.type === \"html\" &&\n /data-stripo-keep=\"stripo-keep-(\\d+)\"/.test(b.content)\n ) {\n const m = /data-stripo-keep=\"stripo-keep-(\\d+)\"/.exec(b.content);\n const i = m ? Number(m[1]) : -1;\n if (i >= 0 && replacements[i] && !used.has(i)) {\n used.add(i);\n out.push(...replacements[i]);\n continue;\n }\n }\n if (b.type === \"paragraph\" && /stripo-keep-/.test(b.content ?? \"\")) {\n const m = /stripo-keep-(\\d+)/.exec(b.content);\n const i = m ? Number(m[1]) : -1;\n if (i >= 0 && replacements[i] && !used.has(i)) {\n used.add(i);\n out.push(...replacements[i]);\n continue;\n }\n }\n out.push(b);\n }\n };\n walk(generic);\n for (let i = 0; i < replacements.length; i++) {\n if (!used.has(i)) out.push(...replacements[i]);\n }\n return out;\n}\n\nexport function convertCompiled(html: string, ctx: ConvertCtx): Block[] {\n const $ = load(html);\n const stripes = topStripes($);\n if (stripes.length === 0) {\n return convertSubtree($.root() as unknown as Cheerio<Element>, $, ctx);\n }\n return stripes.map(($stripe) => {\n const token = stripeToken($stripe) ?? \"es-content\";\n const cols = floatedColumns($stripe, $);\n const { layout, flattenExtra } = columnLayout(cols.length || 1, ctx, token);\n let children: Block[][];\n if (cols.length <= 1) {\n children = [convertSubtree($stripe, $, ctx)];\n } else if (flattenExtra) {\n const slots = cols.slice(0, 2).map(($c) => convertSubtree($c, $, ctx));\n const rest: Block[] = [];\n for (const $c of cols.slice(2)) rest.push(...convertSubtree($c, $, ctx));\n slots.push(rest);\n children = slots;\n } else {\n children = cols.map(($c) => convertSubtree($c, $, ctx));\n }\n if (!flattenExtra) pushEntry(ctx, token, \"section\", \"converted\");\n return withBackground(\n createSectionBlock({ columns: layout, children }),\n stripeFill($stripe),\n );\n });\n}\n","const EDITOR_TOKENS = new Set([\n \"esd-stripe\",\n \"esd-structure\",\n \"esd-container-frame\",\n]);\n\nconst COMPILED_TOKENS = new Set([\n \"es-wrapper\",\n \"es-content-body\",\n \"es-header-body\",\n]);\n\nexport type StripoKind = \"editor\" | \"compiled\";\n\nfunction isTagNameEnd(ch: string | undefined): boolean {\n return ch === undefined || !/[A-Za-z0-9_]/.test(ch);\n}\n\n/**\n * Drop every `tag` element, including a closer with extra attributes or\n * whitespace (`</style >`, `</script foo>`). Linear `indexOf` — a regex\n * here fails `js/bad-tag-filter` or `js/polynomial-redos`, and Cheerio\n * rewrites a bare `<td>` fragment and would drop the attributes we need.\n */\nfunction withoutElements(html: string, tag: string): string {\n const open = `<${tag}`;\n const close = `</${tag}`;\n const lower = html.toLowerCase();\n let out = \"\";\n let pos = 0;\n while (pos < html.length) {\n const start = lower.indexOf(open, pos);\n if (start === -1) {\n out += html.slice(pos);\n break;\n }\n if (!isTagNameEnd(lower[start + open.length])) {\n out += html.slice(pos, start + open.length);\n pos = start + open.length;\n continue;\n }\n out += html.slice(pos, start);\n const gt = html.indexOf(\">\", start);\n if (gt === -1) break;\n const closeAt = lower.indexOf(close, gt + 1);\n if (closeAt === -1) break;\n const closeGt = html.indexOf(\">\", closeAt);\n if (closeGt === -1) break;\n out += \" \";\n pos = closeGt + 1;\n }\n return out;\n}\n\n/**\n * Class tokens from attributes only. Stylesheets and scripts are dropped\n * first so a compiled leftover `.esd-block-html table` rule cannot trip\n * editor detection.\n */\nfunction markupClassTokens(html: string): string[] {\n const stripped = withoutElements(withoutElements(html, \"style\"), \"script\");\n const tokens: string[] = [];\n const re = /\\bclass\\s*=\\s*([\"'])([^\"']*)\\1/gi;\n let m: RegExpExecArray | null;\n while ((m = re.exec(stripped))) {\n tokens.push(...m[2].trim().split(/\\s+/).filter(Boolean));\n }\n return tokens;\n}\n\nfunction isEditorToken(token: string): boolean {\n return EDITOR_TOKENS.has(token) || token.startsWith(\"esd-block-\");\n}\n\n/**\n * Which Stripo HTML surface this is, or null if it is not Stripo markup.\n *\n * Editor-source (`getTemplateData`) carries `esd-*` on elements. Compiled\n * export carries `es-wrapper` / `es-*-body` and never `esd-*` on a class\n * attribute. A CSS-only leftover (`.esd-block-html` in a stylesheet) is not\n * a discriminator — stylesheets are stripped before the check.\n */\nexport function detectStripoKind(html: string): StripoKind | null {\n if (typeof html !== \"string\" || html.trim().length === 0) return null;\n const tokens = markupClassTokens(html);\n if (tokens.some(isEditorToken)) return \"editor\";\n if (tokens.some((t) => COMPILED_TOKENS.has(t))) return \"compiled\";\n return null;\n}\n\nexport function isStripoHtml(html: string): boolean {\n return detectStripoKind(html) !== null;\n}\n","import { load, type Cheerio, type CheerioAPI } from \"cheerio\";\nimport type { Element } from \"domhandler\";\nimport { createHtmlBlock, createSectionBlock } from \"@templatical/types\";\nimport type { Block, ColumnLayout } from \"@templatical/types\";\nimport { isHidden, tokenStartingWith, topLevelWithToken } from \"./classes\";\nimport { colorFromPaint } from \"./css\";\nimport { pushEntry, withBackground, type ConvertCtx } from \"./fragment\";\nimport {\n buttonFrom,\n labelledInnerBlocks,\n menuFrom,\n socialFrom,\n spacerFrom,\n} from \"./labelled\";\n\nfunction columnLayout(count: number, ctx: ConvertCtx): ColumnLayout {\n if (count <= 1) return \"1\";\n if (count === 2) return \"2\";\n if (count === 3) return \"3\";\n pushEntry(\n ctx,\n \"esd-structure\",\n \"section\",\n \"approximated\",\n `Row with ${count} columns was flattened to a single column. Templatical supports up to 3 columns per section.`,\n );\n ctx.warnings.push(\n `Row with ${count} columns was flattened to a single column. Templatical supports up to 3 columns per section.`,\n );\n return \"1\";\n}\n\nfunction esdBlockKind($el: Cheerio<Element>): string | undefined {\n const token = tokenStartingWith($el, \"esd-block-\");\n return token ? token.slice(\"esd-block-\".length) : undefined;\n}\n\nfunction convertEsdBlock(\n $el: Cheerio<Element>,\n $: CheerioAPI,\n ctx: ConvertCtx,\n): Block[] {\n const kind = esdBlockKind($el);\n if (!kind) return labelledInnerBlocks($el, $, ctx);\n if (kind === \"button\") return [buttonFrom($el, \"esd-block-button\", ctx)];\n if (kind === \"spacer\") return [spacerFrom($el, \"esd-block-spacer\", ctx)];\n if (kind === \"social\") return [socialFrom($el, $, \"esd-block-social\", ctx)];\n if (kind === \"menu\") {\n const menu = menuFrom($el, $, \"esd-block-menu\", ctx);\n if (menu) return [menu];\n return labelledInnerBlocks($el, $, ctx);\n }\n if (kind === \"html\") {\n pushEntry(\n ctx,\n \"esd-block-html\",\n \"html\",\n \"html-fallback\",\n \"HTML block preserved as HTML.\",\n );\n return [createHtmlBlock({ content: ($el.html() ?? \"\").trim() })];\n }\n // image / text / anything else: generic HTML mapping of the inner markup\n pushEntry(\n ctx,\n `esd-block-${kind}`,\n kind === \"image\" ? \"image\" : \"paragraph\",\n \"converted\",\n );\n return labelledInnerBlocks($el, $, ctx);\n}\n\nfunction blocksInContainer(\n $frame: Cheerio<Element>,\n $: CheerioAPI,\n ctx: ConvertCtx,\n): Block[] {\n const out: Block[] = [];\n const seen = new Set<Element>();\n $frame.find(\"[class]\").each((_, node) => {\n const $el = $(node) as Cheerio<Element>;\n if (!tokenStartingWith($el, \"esd-block-\")) return;\n const nested = $el\n .parents()\n .toArray()\n .some((p) => tokenStartingWith($(p) as Cheerio<Element>, \"esd-block-\"));\n if (nested) return;\n if (seen.has(node)) return;\n seen.add(node);\n out.push(...convertEsdBlock($el, $, ctx));\n });\n if (out.length === 0) return labelledInnerBlocks($frame, $, ctx);\n return out;\n}\n\nfunction sectionFromStructure(\n $structure: Cheerio<Element>,\n $: CheerioAPI,\n ctx: ConvertCtx,\n): Block {\n const frames = $structure\n .find(\"[class~='esd-container-frame']\")\n .toArray()\n .map((n) => $(n) as Cheerio<Element>)\n .filter(($el) => {\n if (isHidden($el)) return false;\n return (\n $el.parents(\"[class~='esd-container-frame']\").filter((_, p) => {\n return $(p).closest($structure).length > 0;\n }).length === 0\n );\n });\n\n const count = frames.length || 1;\n const layout = columnLayout(count, ctx);\n let children: Block[][];\n if (layout === \"1\") {\n const merged: Block[] = [];\n if (frames.length === 0)\n merged.push(...blocksInContainer($structure, $, ctx));\n else for (const $f of frames) merged.push(...blocksInContainer($f, $, ctx));\n children = [merged];\n } else {\n children = frames.map(($f) => blocksInContainer($f, $, ctx));\n }\n if (layout !== \"1\" || frames.length <= 3) {\n pushEntry(ctx, \"esd-structure\", \"section\", \"converted\");\n }\n return withBackground(\n createSectionBlock({ columns: layout, children }),\n colorFromPaint($structure.attr(\"style\"), $structure.attr(\"bgcolor\")),\n );\n}\n\nexport function convertEditor(html: string, ctx: ConvertCtx): Block[] {\n const $ = load(html);\n const structures = topLevelWithToken($, \"esd-structure\");\n if (structures.length > 0) {\n return structures.map(($s) => sectionFromStructure($s, $, ctx));\n }\n const stripes = topLevelWithToken($, \"esd-stripe\");\n if (stripes.length === 0) {\n return labelledInnerBlocks($.root() as unknown as Cheerio<Element>, $, ctx);\n }\n return stripes.map(($stripe) => {\n const inner = $stripe.find(\"[class~='esd-structure']\");\n if (inner.length > 0) {\n return sectionFromStructure(inner.first() as Cheerio<Element>, $, ctx);\n }\n pushEntry(ctx, \"esd-stripe\", \"section\", \"converted\");\n return withBackground(\n createSectionBlock({\n columns: \"1\",\n children: [blocksInContainer($stripe, $, ctx)],\n }),\n colorFromPaint($stripe.attr(\"style\"), $stripe.attr(\"bgcolor\")),\n );\n });\n}\n","import { load } from \"cheerio\";\nimport { convertHtmlTemplate } from \"@templatical/import-html\";\nimport type { ImportReport, ImportResult } from \"@templatical/import-html\";\nimport { createDefaultTemplateContent } from \"@templatical/types\";\nimport { convertCompiled } from \"./compiled\";\nimport { detectStripoKind } from \"./detect\";\nimport { convertEditor } from \"./editor\";\nimport type { ConvertCtx } from \"./fragment\";\n\nexport interface ConvertStripoOptions {\n /** Sibling CSS from plugin `getTemplateData()`. Ignored on compiled exports. */\n css?: string;\n}\n\nfunction injectCss(html: string, css?: string): string {\n if (!css || css.trim().length === 0) return html;\n const $ = load(html);\n if ($(\"head\").length === 0) {\n $(\"html\").prepend(\"<head></head>\");\n }\n // Style elements serialize as HTML rawtext, so a literal `</style` in the\n // CSS would close the element and the rest would parse as markup — which\n // can flip editor/compiled detection. Neutralize the closer; then set a\n // text node rather than interpolating into an HTML string.\n const safe = css.replace(/<\\/style/gi, \"<\\\\/style\");\n const $style = $(\"<style></style>\").attr(\"data-stripo-css\", \"1\").text(safe);\n $(\"head\").append($style);\n return $.html() ?? html;\n}\n\nfunction summarize(ctx: ConvertCtx): ImportReport {\n const summary = {\n total: ctx.entries.length,\n converted: 0,\n approximated: 0,\n htmlFallback: 0,\n skipped: 0,\n };\n for (const e of ctx.entries) {\n if (e.status === \"converted\") summary.converted++;\n else if (e.status === \"approximated\") summary.approximated++;\n else if (e.status === \"html-fallback\") summary.htmlFallback++;\n else if (e.status === \"skipped\") summary.skipped++;\n }\n return { entries: ctx.entries, warnings: ctx.warnings, summary };\n}\n\n/**\n * Convert a Stripo template to Templatical content.\n *\n * Auto-detects the surface from class attributes:\n * - `esd-*` on elements → plugin/editor `getTemplateData` HTML\n * - `es-wrapper` / `es-*-body` without `esd-*` → compiled File→HTML export\n *\n * Pass `options.css` when converting plugin storage (`getTemplateData().css`).\n */\nexport function convertStripoTemplate(\n html: string,\n options?: ConvertStripoOptions,\n): ImportResult {\n if (typeof html !== \"string\") {\n throw new Error(\n \"Invalid Stripo template: expected a string. Pass the HTML source as a string.\",\n );\n }\n if (html.trim().length === 0) {\n throw new Error(\n \"Invalid Stripo template: input is empty. Pass the HTML source of a Stripo email.\",\n );\n }\n\n const prepared = injectCss(html, options?.css);\n const kind = detectStripoKind(prepared);\n const fallback = convertHtmlTemplate(prepared);\n\n if (!kind) {\n return {\n content: fallback.content,\n report: {\n ...fallback.report,\n warnings: [\n \"No Stripo class attributes found; converted as generic HTML.\",\n ...fallback.report.warnings,\n ],\n },\n };\n }\n\n const ctx: ConvertCtx = { entries: [], warnings: [] };\n const blocks =\n kind === \"editor\"\n ? convertEditor(prepared, ctx)\n : convertCompiled(prepared, ctx);\n\n return {\n content: {\n ...createDefaultTemplateContent(),\n ...fallback.content,\n blocks,\n },\n report: summarize(ctx),\n };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@templatical/import-stripo",
|
|
3
|
+
"description": "Convert Stripo email templates to Templatical format",
|
|
4
|
+
"version": "0.36.0",
|
|
5
|
+
"bugs": "https://github.com/templatical/sdk/issues",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@templatical/import-html": "0.36.0",
|
|
8
|
+
"@templatical/types": "0.36.0",
|
|
9
|
+
"cheerio": "^1.2.0",
|
|
10
|
+
"domhandler": "^6.0.1"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "^25.9.5",
|
|
14
|
+
"typescript": "^6.0.3",
|
|
15
|
+
"vitest": "^5.0.0"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"homepage": "https://templatical.com",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"email",
|
|
29
|
+
"email-template",
|
|
30
|
+
"importer",
|
|
31
|
+
"migration",
|
|
32
|
+
"stripo",
|
|
33
|
+
"templatical"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"module": "./dist/index.js",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/templatical/sdk.git",
|
|
43
|
+
"directory": "packages/import-stripo"
|
|
44
|
+
},
|
|
45
|
+
"type": "module",
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsdown",
|
|
49
|
+
"test": "vitest run --config vitest.config.ts",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|