@templatical/import-topol 0.32.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 +42 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +840 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -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,42 @@
|
|
|
1
|
+
# @templatical/import-topol
|
|
2
|
+
|
|
3
|
+
Convert Topol.io email templates to Templatical format.
|
|
4
|
+
|
|
5
|
+
Reads a Topol design tree — the JSON a Topol project exports — into a Templatical `TemplateContent` tree. Topol's own tag vocabulary already mirrors MJML's (`mj-section`, `mj-column`, `mj-text`, `mj-button`, …), so structural tags become `SectionBlock`s and content tags map to their Templatical block equivalents directly.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @templatical/import-topol
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { convertTopolTemplate } from '@templatical/import-topol';
|
|
17
|
+
|
|
18
|
+
// Topol hands you the design directly from its editor's onSave callback.
|
|
19
|
+
// Fetched from its REST API instead, it sits under "definition" (the
|
|
20
|
+
// template endpoint) or "json" (the predefined-templates endpoint) — not
|
|
21
|
+
// the response object itself.
|
|
22
|
+
const res = await fetch('https://app.topol.io/api/templates/123').then((r) => r.json());
|
|
23
|
+
const { content, report } = convertTopolTemplate(res.definition);
|
|
24
|
+
|
|
25
|
+
console.log(report.summary);
|
|
26
|
+
console.log(report.warnings);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`convertTopolTemplate` also accepts the design serialized as a JSON string.
|
|
30
|
+
|
|
31
|
+
Each entry in `report.entries` carries a `status`:
|
|
32
|
+
|
|
33
|
+
- `converted` — mapped to a Templatical block with no loss of fidelity.
|
|
34
|
+
- `approximated` — mapped to a Templatical block, but something had to be resolved or clamped (e.g. a heading level beyond h4, a column layout with no exact Templatical match, or a social icon size outside 24/32/48px).
|
|
35
|
+
- `html-fallback` — no Templatical block exists for this tag; the raw node is preserved as JSON inside an `HtmlBlock`.
|
|
36
|
+
- `skipped` — reserved for parity with the other `@templatical/import-*` packages; this converter does not currently produce it.
|
|
37
|
+
|
|
38
|
+
See [Migrating from Topol](https://docs.templatical.com/guide/migration-from-topol) for the full tag-mapping table and what does not survive the conversion.
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Type definitions for the Topol.io importer.
|
|
4
|
+
*
|
|
5
|
+
* `ConversionStatus`, `ImportReport`, and `ImportResult` are structurally
|
|
6
|
+
* identical to the same types in `import-html`, `import-mjml`,
|
|
7
|
+
* `import-beefree`, and `import-unlayer`, so a caller can treat those three
|
|
8
|
+
* types uniformly across every `@templatical/import-*` package.
|
|
9
|
+
* `ImportReportEntry` is not: its first field is `sourceTag` here, the same
|
|
10
|
+
* name `import-html` and `import-mjml` use, but `import-beefree` calls it
|
|
11
|
+
* `beeFreeModuleType` and `import-unlayer` calls it `unlayerContentType` — the
|
|
12
|
+
* one field a caller reading across every importer would have to branch on.
|
|
13
|
+
*/
|
|
14
|
+
type ConversionStatus = "converted" | "approximated" | "html-fallback" | "skipped";
|
|
15
|
+
interface ImportReportEntry {
|
|
16
|
+
/** The source Topol tag name (e.g. "mj-text", "mj-social", "mj-gif"). */
|
|
17
|
+
sourceTag: string;
|
|
18
|
+
/** The Templatical block type produced, or null if skipped. */
|
|
19
|
+
templaticalBlockType: string | null;
|
|
20
|
+
status: ConversionStatus;
|
|
21
|
+
note?: string;
|
|
22
|
+
}
|
|
23
|
+
interface ImportReport {
|
|
24
|
+
entries: ImportReportEntry[];
|
|
25
|
+
warnings: string[];
|
|
26
|
+
summary: {
|
|
27
|
+
total: number;
|
|
28
|
+
converted: number;
|
|
29
|
+
approximated: number;
|
|
30
|
+
htmlFallback: number;
|
|
31
|
+
skipped: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
interface ImportResult {
|
|
35
|
+
content: import("@templatical/types").TemplateContent;
|
|
36
|
+
report: ImportReport;
|
|
37
|
+
}
|
|
38
|
+
/** A Topol attribute value. Topol writes explicit `null` for "unset". */
|
|
39
|
+
type TopolAttrValue = string | number | boolean | null | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* One node of a Topol design tree.
|
|
42
|
+
*
|
|
43
|
+
* `content` sits at the top level rather than inside `attributes`, and carries
|
|
44
|
+
* rich-text HTML for `mj-text` and a button label for `mj-button`.
|
|
45
|
+
*/
|
|
46
|
+
interface TopolNode {
|
|
47
|
+
tagName: string;
|
|
48
|
+
attributes?: Record<string, TopolAttrValue | Record<string, TopolAttrValue>>;
|
|
49
|
+
children?: TopolNode[];
|
|
50
|
+
content?: string;
|
|
51
|
+
uid?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
/** The design root — the node whose `tagName` is `mj-global-style`. */
|
|
55
|
+
interface TopolDesign extends TopolNode {
|
|
56
|
+
tagName: string;
|
|
57
|
+
fonts?: unknown;
|
|
58
|
+
style?: unknown;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/converter.d.ts
|
|
62
|
+
/**
|
|
63
|
+
* Convert a Topol.io design into a Templatical template.
|
|
64
|
+
*
|
|
65
|
+
* The input is the design object itself — the node whose `tagName` is
|
|
66
|
+
* `mj-global-style`, not a whole API response. Topol's editor hands it to
|
|
67
|
+
* you directly as the first argument to `onSave`; its REST APIs wrap it
|
|
68
|
+
* instead, under `definition` on the template-retrieval endpoint or `json`
|
|
69
|
+
* (alongside `html`) on the predefined-templates endpoint. That is
|
|
70
|
+
* documented rather than sniffed: guessing which field holds the design
|
|
71
|
+
* risks importing an envelope's `html`, which belongs to a different
|
|
72
|
+
* package entirely.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const { content, report } = convertTopolTemplate(design);
|
|
77
|
+
* const editor = init({ container: '#editor', content });
|
|
78
|
+
* console.log(report.summary);
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function convertTopolTemplate(design: TopolDesign | string): ImportResult;
|
|
82
|
+
//#endregion
|
|
83
|
+
export type { ConversionStatus, ImportReport, ImportReportEntry, ImportResult };
|
|
84
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import { DEFAULT_TEMPLATE_DEFAULTS, createButtonBlock, createDefaultTemplateContent, createDividerBlock, createHtmlBlock, createImageBlock, createParagraphBlock, createSectionBlock, createSocialIconsBlock, createSpacerBlock, createTitleBlock, findOpenTagEnd, generateId } from "@templatical/types";
|
|
2
|
+
//#region src/attribute-parser.ts
|
|
3
|
+
/**
|
|
4
|
+
* Read one attribute as a string, treating Topol's explicit `null` — and an
|
|
5
|
+
* empty string — as absent.
|
|
6
|
+
*
|
|
7
|
+
* Topol writes literal `null` for an unset attribute rather than omitting the
|
|
8
|
+
* key, so a `!== undefined` check would report it as present and write a null
|
|
9
|
+
* into the block. Every optional-field read goes through here.
|
|
10
|
+
*/
|
|
11
|
+
function attr(node, key) {
|
|
12
|
+
const raw = node.attributes?.[key];
|
|
13
|
+
if (raw === null || raw === void 0) return void 0;
|
|
14
|
+
if (typeof raw === "object") return void 0;
|
|
15
|
+
const value = String(raw);
|
|
16
|
+
return value === "" ? void 0 : value;
|
|
17
|
+
}
|
|
18
|
+
/** Read one attribute as a number, or `undefined` when unset or non-numeric. */
|
|
19
|
+
function numAttr(node, key) {
|
|
20
|
+
const value = attr(node, key);
|
|
21
|
+
if (value === void 0) return void 0;
|
|
22
|
+
const parsed = Number(value);
|
|
23
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse a px-like value into a rounded integer. Returns 0 for a missing value
|
|
27
|
+
* and for units the block model cannot express (em, rem, %).
|
|
28
|
+
*
|
|
29
|
+
* Trims first, then matches with no `\s*` on either side of the optional
|
|
30
|
+
* `px` unit. A trailing run of spaces can otherwise be claimed by either the
|
|
31
|
+
* surrounding `\s*` or by backtracking out of the digit/unit match, and a
|
|
32
|
+
* *failing* match (no `px`, no digits — anything not shaped like a length)
|
|
33
|
+
* retries every such split before giving up: `"0" + " ".repeat(n) + "x"` is
|
|
34
|
+
* polynomial in `n` against that pattern. Trimming first removes the only
|
|
35
|
+
* whitespace the pattern needs to tolerate, so the match itself has nothing
|
|
36
|
+
* ambiguous left to backtrack over.
|
|
37
|
+
*/
|
|
38
|
+
function parsePxValue(value) {
|
|
39
|
+
if (value === void 0 || value === null || value === "") return 0;
|
|
40
|
+
if (typeof value === "number") return Math.round(value);
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
const match = /^(-?\d+(?:\.\d+)?)(?:px)?$/.exec(trimmed);
|
|
43
|
+
return match ? Math.round(parseFloat(match[1])) : 0;
|
|
44
|
+
}
|
|
45
|
+
const NAMED_COLORS = {
|
|
46
|
+
black: "#000000",
|
|
47
|
+
white: "#ffffff",
|
|
48
|
+
red: "#ff0000",
|
|
49
|
+
green: "#008000",
|
|
50
|
+
blue: "#0000ff",
|
|
51
|
+
yellow: "#ffff00",
|
|
52
|
+
cyan: "#00ffff",
|
|
53
|
+
magenta: "#ff00ff",
|
|
54
|
+
gray: "#808080",
|
|
55
|
+
grey: "#808080",
|
|
56
|
+
silver: "#c0c0c0",
|
|
57
|
+
maroon: "#800000",
|
|
58
|
+
olive: "#808000",
|
|
59
|
+
lime: "#00ff00",
|
|
60
|
+
aqua: "#00ffff",
|
|
61
|
+
teal: "#008080",
|
|
62
|
+
navy: "#000080",
|
|
63
|
+
fuchsia: "#ff00ff",
|
|
64
|
+
purple: "#800080",
|
|
65
|
+
orange: "#ffa500",
|
|
66
|
+
pink: "#ffc0cb"
|
|
67
|
+
};
|
|
68
|
+
function rgbToHex(r, g, b) {
|
|
69
|
+
const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
70
|
+
const hex = (n) => clamp(n).toString(16).padStart(2, "0");
|
|
71
|
+
return `#${hex(r)}${hex(g)}${hex(b)}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Normalize a colour to 6-digit lowercase hex.
|
|
75
|
+
*
|
|
76
|
+
* Returns `""` for transparent/inherit/none and for anything unrecognised —
|
|
77
|
+
* the empty string is the block model's "unset", which the colour pickers
|
|
78
|
+
* clear to, so it is meaningfully different from returning a default.
|
|
79
|
+
*/
|
|
80
|
+
function parseColor(value) {
|
|
81
|
+
if (!value) return "";
|
|
82
|
+
const trimmed = value.trim().toLowerCase();
|
|
83
|
+
if (trimmed === "transparent" || trimmed === "inherit" || trimmed === "none") return "";
|
|
84
|
+
if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed;
|
|
85
|
+
if (/^#[0-9a-f]{3}$/.test(trimmed)) {
|
|
86
|
+
const [, r, g, b] = trimmed;
|
|
87
|
+
return `#${r}${r}${g}${g}${b}${b}`;
|
|
88
|
+
}
|
|
89
|
+
const rgb = trimmed.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[\d.]+\s*)?\)$/);
|
|
90
|
+
if (rgb) return rgbToHex(+rgb[1], +rgb[2], +rgb[3]);
|
|
91
|
+
return NAMED_COLORS[trimmed] ?? "";
|
|
92
|
+
}
|
|
93
|
+
function expandShorthand(value) {
|
|
94
|
+
if (!value) return {
|
|
95
|
+
top: 0,
|
|
96
|
+
right: 0,
|
|
97
|
+
bottom: 0,
|
|
98
|
+
left: 0
|
|
99
|
+
};
|
|
100
|
+
const v = value.trim().split(/\s+/).map((p) => parsePxValue(p));
|
|
101
|
+
switch (v.length) {
|
|
102
|
+
case 1: return {
|
|
103
|
+
top: v[0],
|
|
104
|
+
right: v[0],
|
|
105
|
+
bottom: v[0],
|
|
106
|
+
left: v[0]
|
|
107
|
+
};
|
|
108
|
+
case 2: return {
|
|
109
|
+
top: v[0],
|
|
110
|
+
right: v[1],
|
|
111
|
+
bottom: v[0],
|
|
112
|
+
left: v[1]
|
|
113
|
+
};
|
|
114
|
+
case 3: return {
|
|
115
|
+
top: v[0],
|
|
116
|
+
right: v[1],
|
|
117
|
+
bottom: v[2],
|
|
118
|
+
left: v[1]
|
|
119
|
+
};
|
|
120
|
+
default: return {
|
|
121
|
+
top: v[0],
|
|
122
|
+
right: v[1],
|
|
123
|
+
bottom: v[2],
|
|
124
|
+
left: v[3]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Read a node's padding, handling both forms Topol emits: a shorthand string
|
|
130
|
+
* (`"9px 9px 9px 9px"`, used by most leaves) and four separate bare-number
|
|
131
|
+
* keys (`padding-top: 24`, used by `mj-divider`). A separate key wins over the
|
|
132
|
+
* shorthand for its own side.
|
|
133
|
+
*/
|
|
134
|
+
function parsePadding(node) {
|
|
135
|
+
const base = expandShorthand(attr(node, "padding"));
|
|
136
|
+
const side = (key, fallback) => {
|
|
137
|
+
const value = attr(node, key);
|
|
138
|
+
return value === void 0 ? fallback : parsePxValue(value);
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
top: side("padding-top", base.top),
|
|
142
|
+
right: side("padding-right", base.right),
|
|
143
|
+
bottom: side("padding-bottom", base.bottom),
|
|
144
|
+
left: side("padding-left", base.left)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Read a percentage. Returns `null` when the value is not a percentage, so a
|
|
149
|
+
* caller can tell "no width given" from "0%" — column matching depends on that
|
|
150
|
+
* distinction.
|
|
151
|
+
*/
|
|
152
|
+
function parsePercent(value) {
|
|
153
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
154
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
155
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*%$/);
|
|
156
|
+
return match ? parseFloat(match[1]) : null;
|
|
157
|
+
}
|
|
158
|
+
/** Strip quotes and return the first family in a stack. */
|
|
159
|
+
function parseFontFamily(value) {
|
|
160
|
+
if (!value) return "";
|
|
161
|
+
return value.split(",")[0].trim().replace(/^['"]|['"]$/g, "");
|
|
162
|
+
}
|
|
163
|
+
/** Narrow an alignment to the three the block model accepts. */
|
|
164
|
+
function parseAlignment(value, fallback = "left") {
|
|
165
|
+
const v = (value ?? "").trim().toLowerCase();
|
|
166
|
+
return v === "left" || v === "center" || v === "right" ? v : fallback;
|
|
167
|
+
}
|
|
168
|
+
/** Narrow a border style to the three `DividerBlock.lineStyle` accepts. */
|
|
169
|
+
function parseBorderStyle(value) {
|
|
170
|
+
const v = (value ?? "").trim().toLowerCase();
|
|
171
|
+
return v === "dashed" || v === "dotted" ? v : "solid";
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/global-style.ts
|
|
175
|
+
/**
|
|
176
|
+
* `DEFAULT_TEMPLATE_DEFAULTS` is typed `Partial<TemplateSettings>` so a
|
|
177
|
+
* consumer can override any subset, but its own literal always sets the six
|
|
178
|
+
* required fields. Narrowing once here is what lets each read come out as
|
|
179
|
+
* `number`/`string`/`boolean` rather than `| undefined`. A plain typed
|
|
180
|
+
* assignment cannot substitute: `Partial<T>` is not assignable to
|
|
181
|
+
* `Required<Pick<T, …>>`, so `tsc` rejects it outright.
|
|
182
|
+
*/
|
|
183
|
+
const REQUIRED_DEFAULTS = DEFAULT_TEMPLATE_DEFAULTS;
|
|
184
|
+
/** A per-tag default, or undefined when the design sets none. */
|
|
185
|
+
function tagDefault(style, tag, key) {
|
|
186
|
+
return style.tagDefaults[tag]?.[key];
|
|
187
|
+
}
|
|
188
|
+
/** A per-selector default (`""` is the document default), or undefined. */
|
|
189
|
+
function selectorDefault(style, selector, key) {
|
|
190
|
+
return style.selectorDefaults[selector]?.[key];
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Split the root's `attributes` into settings, per-tag defaults and
|
|
194
|
+
* per-selector defaults.
|
|
195
|
+
*
|
|
196
|
+
* The root mixes two idioms: `<selector>:<property>` flat keys (`:color`,
|
|
197
|
+
* `a:color`, `h1:font-family`) and `<tag>: { … }` nested objects, which are
|
|
198
|
+
* the analogue of MJML's `mj-attributes`.
|
|
199
|
+
*/
|
|
200
|
+
function readGlobalStyle(root, container, warnings) {
|
|
201
|
+
const tagDefaults = Object.create(null);
|
|
202
|
+
const selectorDefaults = Object.create(null);
|
|
203
|
+
for (const [key, value] of Object.entries(root.attributes ?? {})) {
|
|
204
|
+
if (value !== null && typeof value === "object") {
|
|
205
|
+
const bucket = tagDefaults[key] ??= Object.create(null);
|
|
206
|
+
for (const [k, v] of Object.entries(value)) if (v !== null && v !== void 0) bucket[k] = String(v);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const colon = key.indexOf(":");
|
|
210
|
+
if (colon === -1) continue;
|
|
211
|
+
const selector = key.slice(0, colon);
|
|
212
|
+
const property = key.slice(colon + 1);
|
|
213
|
+
if (value === null || value === void 0) continue;
|
|
214
|
+
const bucket = selectorDefaults[selector] ??= Object.create(null);
|
|
215
|
+
bucket[property] = String(value);
|
|
216
|
+
}
|
|
217
|
+
const width = parsePxValue(attr(root, "containerWidth")) || REQUIRED_DEFAULTS.width;
|
|
218
|
+
const backgroundColor = (container ? parseColor(attr(container, "background-color")) : "") || REQUIRED_DEFAULTS.backgroundColor;
|
|
219
|
+
const textColor = parseColor(selectorDefaults[""]?.color) || REQUIRED_DEFAULTS.textColor;
|
|
220
|
+
const fontFamily = parseFontFamily(selectorDefaults[""]?.["font-family"]) || parseFontFamily(attr(root, "fonts")) || REQUIRED_DEFAULTS.fontFamily;
|
|
221
|
+
const linkColor = parseColor(selectorDefaults["a"]?.color);
|
|
222
|
+
const lineHeight = selectorDefaults[""]?.["line-height"];
|
|
223
|
+
if (lineHeight !== void 0) warnings.push(`Dropped the document line-height (${lineHeight}) — Templatical has no document-level line-height setting.`);
|
|
224
|
+
return {
|
|
225
|
+
settings: {
|
|
226
|
+
width,
|
|
227
|
+
backgroundColor,
|
|
228
|
+
textColor,
|
|
229
|
+
linkUnderline: REQUIRED_DEFAULTS.linkUnderline,
|
|
230
|
+
fontFamily,
|
|
231
|
+
locale: REQUIRED_DEFAULTS.locale,
|
|
232
|
+
...linkColor ? { linkColor } : {}
|
|
233
|
+
},
|
|
234
|
+
tagDefaults,
|
|
235
|
+
selectorDefaults
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/content-inference.ts
|
|
240
|
+
/**
|
|
241
|
+
* A single heading element wrapping the whole content, captured with its level
|
|
242
|
+
* and inner markup. Anchored at both ends so a heading with a sibling does not
|
|
243
|
+
* match — that case is a paragraph.
|
|
244
|
+
*/
|
|
245
|
+
const SOLE_HEADING = /^<h([1-6])(?:\s[^>]*)?>([\s\S]*)<\/h\1>$/i;
|
|
246
|
+
const BLOCK_ROOT = /^<(p|h[1-6]|ul|ol|blockquote|div|table)\b/i;
|
|
247
|
+
/**
|
|
248
|
+
* Resolve a Topol `content` string to a Title or a Paragraph.
|
|
249
|
+
*
|
|
250
|
+
* Topol has no menu or table construct, so this is a two-way choice — the MJML
|
|
251
|
+
* importer needs four arms because `mj-text` is the output of four different
|
|
252
|
+
* block renderers there. Paragraph is the terminal arm and always reachable,
|
|
253
|
+
* so this function is total.
|
|
254
|
+
*/
|
|
255
|
+
function inferTextBlock(content) {
|
|
256
|
+
const trimmed = (content ?? "").trim();
|
|
257
|
+
if (trimmed === "") return {
|
|
258
|
+
kind: "paragraph",
|
|
259
|
+
html: "<p></p>"
|
|
260
|
+
};
|
|
261
|
+
const heading = trimmed.match(SOLE_HEADING);
|
|
262
|
+
if (heading) {
|
|
263
|
+
const source = Number(heading[1]);
|
|
264
|
+
return {
|
|
265
|
+
kind: "title",
|
|
266
|
+
level: Math.min(source, 4),
|
|
267
|
+
inner: heading[2].trim(),
|
|
268
|
+
...source > 4 ? { clampedFrom: source } : {}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
kind: "paragraph",
|
|
273
|
+
html: BLOCK_ROOT.test(trimmed) ? trimmed : `<p>${trimmed}</p>`
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const ENTITIES = {
|
|
277
|
+
"&": "&",
|
|
278
|
+
"<": "<",
|
|
279
|
+
">": ">",
|
|
280
|
+
""": "\"",
|
|
281
|
+
"'": "'",
|
|
282
|
+
"'": "'",
|
|
283
|
+
" ": " "
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Remove every `<tag>` from `html`, keeping the text between them.
|
|
287
|
+
*
|
|
288
|
+
* A single-pass linear scan rather than `/<[^>]*>/g`: that regex is
|
|
289
|
+
* polynomial over input with many `<` starts and no closing `>`, since
|
|
290
|
+
* `[^>]*` backtracks at every one of them — `"<".repeat(n)` is quadratic in
|
|
291
|
+
* `n`. `findOpenTagEnd` (from `@templatical/types`) resolves each `<` with a
|
|
292
|
+
* bounded, quote-aware forward scan instead, so the walk stays O(n): every
|
|
293
|
+
* character is visited at most once by `indexOf` and once by
|
|
294
|
+
* `findOpenTagEnd`, because the cursor only moves forward and an
|
|
295
|
+
* unterminated tag ends the walk rather than retrying from inside it. On
|
|
296
|
+
* that unterminated case the remainder of the string — the dangling `<`
|
|
297
|
+
* included — is emitted as-is, matching what `/<[^>]*>/g` does with
|
|
298
|
+
* malformed markup: it can never complete a match without a `>`, so the
|
|
299
|
+
* text is left untouched from that point on.
|
|
300
|
+
*/
|
|
301
|
+
function removeTags(html) {
|
|
302
|
+
let out = "";
|
|
303
|
+
let i = 0;
|
|
304
|
+
while (i < html.length) {
|
|
305
|
+
const open = html.indexOf("<", i);
|
|
306
|
+
if (open === -1) {
|
|
307
|
+
out += html.substring(i);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
out += html.substring(i, open);
|
|
311
|
+
const tagEnd = findOpenTagEnd(html, open + 1);
|
|
312
|
+
if (tagEnd === -1) {
|
|
313
|
+
out += html.substring(open);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
i = tagEnd + 1;
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The visible text of a markup fragment, for a button label — `ButtonBlock.text`
|
|
322
|
+
* is a plain string, so the markup Topol stores in `content` cannot travel.
|
|
323
|
+
*/
|
|
324
|
+
function stripTags(html) {
|
|
325
|
+
if (!html) return "";
|
|
326
|
+
return removeTags(html).replace(/&(?:amp|lt|gt|quot|#39|apos|nbsp);/g, (m) => ENTITIES[m] ?? m).replace(/\s+/g, " ").trim();
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/block-mapper.ts
|
|
330
|
+
/**
|
|
331
|
+
* Read a node's own attribute, falling back to the design's per-tag then
|
|
332
|
+
* per-selector default. Returns undefined when nothing sets it, so the caller
|
|
333
|
+
* can omit the field and let the block factory's default stand.
|
|
334
|
+
*/
|
|
335
|
+
function resolved(node, key, ctx, selector) {
|
|
336
|
+
return attr(node, key) ?? tagDefault(ctx.style, node.tagName, key) ?? (selector === void 0 ? void 0 : selectorDefault(ctx.style, selector, key));
|
|
337
|
+
}
|
|
338
|
+
function baseStyles(node) {
|
|
339
|
+
const backgroundColor = parseColor(attr(node, "background-color"));
|
|
340
|
+
return { styles: {
|
|
341
|
+
padding: parsePadding(node),
|
|
342
|
+
...backgroundColor ? { backgroundColor } : {}
|
|
343
|
+
} };
|
|
344
|
+
}
|
|
345
|
+
function convertText(node, ctx) {
|
|
346
|
+
const inferred = inferTextBlock(node.content);
|
|
347
|
+
if (inferred.kind === "paragraph") return {
|
|
348
|
+
block: createParagraphBlock({
|
|
349
|
+
content: inferred.html,
|
|
350
|
+
...baseStyles(node)
|
|
351
|
+
}),
|
|
352
|
+
entry: {
|
|
353
|
+
sourceTag: "mj-text",
|
|
354
|
+
templaticalBlockType: "paragraph",
|
|
355
|
+
status: "converted"
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const selector = `h${inferred.level}`;
|
|
359
|
+
const color = parseColor(resolved(node, "color", ctx, selector));
|
|
360
|
+
const fontFamily = parseFontFamily(resolved(node, "font-family", ctx, selector));
|
|
361
|
+
return {
|
|
362
|
+
block: createTitleBlock({
|
|
363
|
+
content: inferred.inner,
|
|
364
|
+
level: inferred.level,
|
|
365
|
+
textAlign: parseAlignment(attr(node, "align"), "left"),
|
|
366
|
+
...color ? { color } : {},
|
|
367
|
+
...fontFamily ? { fontFamily } : {},
|
|
368
|
+
...baseStyles(node)
|
|
369
|
+
}),
|
|
370
|
+
entry: {
|
|
371
|
+
sourceTag: "mj-text",
|
|
372
|
+
templaticalBlockType: "title",
|
|
373
|
+
status: inferred.clampedFrom ? "approximated" : "converted",
|
|
374
|
+
...inferred.clampedFrom ? { note: `Heading level h${inferred.clampedFrom} clamped to 4 — Templatical titles support h1-h4.` } : {}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function convertButton(node, ctx) {
|
|
379
|
+
const text = stripTags(node.content);
|
|
380
|
+
if (!text) return null;
|
|
381
|
+
const backgroundColor = parseColor(resolved(node, "background-color", ctx, "button"));
|
|
382
|
+
const textColor = parseColor(resolved(node, "color", ctx, "button"));
|
|
383
|
+
const fontFamily = parseFontFamily(resolved(node, "font-family", ctx, "button"));
|
|
384
|
+
const fontSizeRaw = resolved(node, "font-size", ctx);
|
|
385
|
+
const radiusRaw = resolved(node, "border-radius", ctx);
|
|
386
|
+
return {
|
|
387
|
+
block: createButtonBlock({
|
|
388
|
+
text,
|
|
389
|
+
url: attr(node, "href") ?? "",
|
|
390
|
+
...backgroundColor ? { backgroundColor } : {},
|
|
391
|
+
...textColor ? { textColor } : {},
|
|
392
|
+
...fontFamily ? { fontFamily } : {},
|
|
393
|
+
...fontSizeRaw === void 0 ? {} : { fontSize: parsePxValue(fontSizeRaw) },
|
|
394
|
+
...radiusRaw === void 0 ? {} : { borderRadius: parsePxValue(radiusRaw) },
|
|
395
|
+
align: parseAlignment(attr(node, "align"), "center"),
|
|
396
|
+
...baseStyles(node)
|
|
397
|
+
}),
|
|
398
|
+
entry: {
|
|
399
|
+
sourceTag: node.tagName,
|
|
400
|
+
templaticalBlockType: "button",
|
|
401
|
+
status: "converted"
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function convertImage(node, ctx, sourceTag) {
|
|
406
|
+
const src = attr(node, "src");
|
|
407
|
+
if (!src) return null;
|
|
408
|
+
const widthPercent = numAttr(node, "widthPercent");
|
|
409
|
+
const pxWidth = numAttr(node, "width");
|
|
410
|
+
const href = attr(node, "href");
|
|
411
|
+
const width = widthPercent === 100 || pxWidth === void 0 || pxWidth >= ctx.columnWidth ? "full" : Math.round(pxWidth);
|
|
412
|
+
const isGif = sourceTag === "mj-gif";
|
|
413
|
+
return {
|
|
414
|
+
block: createImageBlock({
|
|
415
|
+
src,
|
|
416
|
+
alt: attr(node, "alt") ?? "",
|
|
417
|
+
width,
|
|
418
|
+
align: parseAlignment(attr(node, "align"), "center"),
|
|
419
|
+
...href ? { linkUrl: href } : {},
|
|
420
|
+
...baseStyles(node)
|
|
421
|
+
}),
|
|
422
|
+
entry: {
|
|
423
|
+
sourceTag,
|
|
424
|
+
templaticalBlockType: "image",
|
|
425
|
+
status: isGif ? "approximated" : "converted",
|
|
426
|
+
...isGif ? { note: "<mj-gif> imported as an image block; Templatical has no dedicated GIF block." } : {}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function convertSpacer(node) {
|
|
431
|
+
const height = attr(node, "height");
|
|
432
|
+
return {
|
|
433
|
+
block: createSpacerBlock({
|
|
434
|
+
...height === void 0 ? {} : { height: parsePxValue(height) },
|
|
435
|
+
...baseStyles(node)
|
|
436
|
+
}),
|
|
437
|
+
entry: {
|
|
438
|
+
sourceTag: "mj-spacer",
|
|
439
|
+
templaticalBlockType: "spacer",
|
|
440
|
+
status: "converted"
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function convertDivider(node) {
|
|
445
|
+
const color = parseColor(attr(node, "border-color"));
|
|
446
|
+
const thickness = attr(node, "border-width");
|
|
447
|
+
return {
|
|
448
|
+
block: createDividerBlock({
|
|
449
|
+
lineStyle: parseBorderStyle(attr(node, "border-style")),
|
|
450
|
+
...color ? { color } : {},
|
|
451
|
+
...thickness === void 0 ? {} : { thickness: parsePxValue(thickness) },
|
|
452
|
+
...baseStyles(node)
|
|
453
|
+
}),
|
|
454
|
+
entry: {
|
|
455
|
+
sourceTag: "mj-divider",
|
|
456
|
+
templaticalBlockType: "divider",
|
|
457
|
+
status: "converted"
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Convert one leaf node to a block.
|
|
463
|
+
*
|
|
464
|
+
* Returns `null` for a node that produces nothing *and* warrants no report
|
|
465
|
+
* entry — an image with no `src`, a button with no label. `mj-social` is not
|
|
466
|
+
* handled here; the converter routes it to `convertSocial`.
|
|
467
|
+
*/
|
|
468
|
+
function convertLeaf(node, ctx) {
|
|
469
|
+
switch (node.tagName) {
|
|
470
|
+
case "mj-text": return convertText(node, ctx);
|
|
471
|
+
case "mj-button": return convertButton(node, ctx);
|
|
472
|
+
case "mj-image": return convertImage(node, ctx, "mj-image");
|
|
473
|
+
case "mj-gif": return convertImage(node, ctx, "mj-gif");
|
|
474
|
+
case "mj-spacer": return convertSpacer(node);
|
|
475
|
+
case "mj-divider": return convertDivider(node);
|
|
476
|
+
default: return {
|
|
477
|
+
block: createHtmlBlock({
|
|
478
|
+
content: JSON.stringify(node, null, 2),
|
|
479
|
+
...baseStyles(node)
|
|
480
|
+
}),
|
|
481
|
+
entry: {
|
|
482
|
+
sourceTag: node.tagName,
|
|
483
|
+
templaticalBlockType: "html",
|
|
484
|
+
status: "html-fallback",
|
|
485
|
+
note: `<${node.tagName}> has no Templatical block equivalent; the original node is preserved as JSON.`
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/social-mapper.ts
|
|
492
|
+
/**
|
|
493
|
+
* Exhaustive over `SocialPlatform` on purpose: adding a member to that union
|
|
494
|
+
* without adding it here is a compile error, so this cannot silently fall back
|
|
495
|
+
* to "website" for a platform the block model gained.
|
|
496
|
+
*/
|
|
497
|
+
const KNOWN_PLATFORMS = {
|
|
498
|
+
facebook: true,
|
|
499
|
+
twitter: true,
|
|
500
|
+
instagram: true,
|
|
501
|
+
linkedin: true,
|
|
502
|
+
youtube: true,
|
|
503
|
+
tiktok: true,
|
|
504
|
+
pinterest: true,
|
|
505
|
+
email: true,
|
|
506
|
+
whatsapp: true,
|
|
507
|
+
telegram: true,
|
|
508
|
+
discord: true,
|
|
509
|
+
snapchat: true,
|
|
510
|
+
reddit: true,
|
|
511
|
+
github: true,
|
|
512
|
+
dribbble: true,
|
|
513
|
+
behance: true,
|
|
514
|
+
website: true
|
|
515
|
+
};
|
|
516
|
+
const ALIASES = {
|
|
517
|
+
x: "twitter",
|
|
518
|
+
"x-twitter": "twitter"
|
|
519
|
+
};
|
|
520
|
+
const ICON_SIZES = [
|
|
521
|
+
[24, "small"],
|
|
522
|
+
[32, "medium"],
|
|
523
|
+
[48, "large"]
|
|
524
|
+
];
|
|
525
|
+
const KNOWN_STYLES = /* @__PURE__ */ new Set([
|
|
526
|
+
"solid",
|
|
527
|
+
"outlined",
|
|
528
|
+
"rounded",
|
|
529
|
+
"square",
|
|
530
|
+
"circle"
|
|
531
|
+
]);
|
|
532
|
+
const STYLE_ALIASES = { outlinedbw: "outlined" };
|
|
533
|
+
function normalizePlatform(raw) {
|
|
534
|
+
const cleaned = raw.trim().toLowerCase();
|
|
535
|
+
if (!cleaned) return null;
|
|
536
|
+
if (ALIASES[cleaned]) return ALIASES[cleaned];
|
|
537
|
+
return cleaned in KNOWN_PLATFORMS ? cleaned : null;
|
|
538
|
+
}
|
|
539
|
+
function nearestSize(px) {
|
|
540
|
+
let best = ICON_SIZES[1];
|
|
541
|
+
let gap = Infinity;
|
|
542
|
+
for (const candidate of ICON_SIZES) {
|
|
543
|
+
const d = Math.abs(candidate[0] - px);
|
|
544
|
+
if (d < gap) {
|
|
545
|
+
gap = d;
|
|
546
|
+
best = candidate;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
size: best[1],
|
|
551
|
+
exact: gap === 0
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/** The style segment of `https://…/social-icos/<style>/`, when it names one. */
|
|
555
|
+
function styleFromBaseUrl(baseUrl) {
|
|
556
|
+
if (!baseUrl) return void 0;
|
|
557
|
+
const segment = baseUrl.split("?")[0].split("/").filter(Boolean).at(-1) ?? "";
|
|
558
|
+
if (KNOWN_STYLES.has(segment)) return segment;
|
|
559
|
+
return STYLE_ALIASES[segment];
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Convert Topol's MJML v3 social block.
|
|
563
|
+
*
|
|
564
|
+
* `display` decides which icons render and in what order; a platform with
|
|
565
|
+
* `*-href` set but absent from `display` was not shown in the email and is
|
|
566
|
+
* not imported.
|
|
567
|
+
*/
|
|
568
|
+
function convertSocial(node, _ctx) {
|
|
569
|
+
const display = attr(node, "display");
|
|
570
|
+
if (!display) return null;
|
|
571
|
+
const notes = [];
|
|
572
|
+
const icons = [];
|
|
573
|
+
for (const token of display.trim().split(/\s+/)) {
|
|
574
|
+
const rawName = token.split(":")[0];
|
|
575
|
+
if (!rawName) continue;
|
|
576
|
+
const url = attr(node, `${rawName}-href`);
|
|
577
|
+
if (!url) continue;
|
|
578
|
+
const platform = normalizePlatform(rawName);
|
|
579
|
+
if (!platform) notes.push(`Unrecognised social platform "${rawName}" mapped to "website".`);
|
|
580
|
+
icons.push({
|
|
581
|
+
id: generateId(),
|
|
582
|
+
platform: platform ?? "website",
|
|
583
|
+
url
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
if (icons.length === 0) return null;
|
|
587
|
+
const iconStyle = styleFromBaseUrl(attr(node, "base-url"));
|
|
588
|
+
const declaredSize = attr(node, "icon-size");
|
|
589
|
+
let iconSize;
|
|
590
|
+
if (declaredSize !== void 0) {
|
|
591
|
+
const resolved = nearestSize(parsePxValue(declaredSize));
|
|
592
|
+
iconSize = resolved.size;
|
|
593
|
+
if (!resolved.exact) notes.push(`Icon size ${declaredSize} is not one of 24/32/48px; resolved to "${resolved.size}".`);
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
block: createSocialIconsBlock({
|
|
597
|
+
icons,
|
|
598
|
+
align: parseAlignment(attr(node, "align"), "center"),
|
|
599
|
+
...iconSize ? { iconSize } : {},
|
|
600
|
+
...iconStyle ? { iconStyle } : {},
|
|
601
|
+
...baseStyles(node)
|
|
602
|
+
}),
|
|
603
|
+
entry: {
|
|
604
|
+
sourceTag: "mj-social",
|
|
605
|
+
templaticalBlockType: "social",
|
|
606
|
+
status: notes.length > 0 ? "approximated" : "converted",
|
|
607
|
+
...notes.length > 0 ? { note: notes.join(" ") } : {}
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/section-builder.ts
|
|
613
|
+
const LAYOUT_SHAPES = [
|
|
614
|
+
{
|
|
615
|
+
layout: "1",
|
|
616
|
+
percents: [100]
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
layout: "2",
|
|
620
|
+
percents: [50, 50]
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
layout: "1-2",
|
|
624
|
+
percents: [33.33, 66.67]
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
layout: "2-1",
|
|
628
|
+
percents: [66.67, 33.33]
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
layout: "3",
|
|
632
|
+
percents: [
|
|
633
|
+
33.33,
|
|
634
|
+
33.33,
|
|
635
|
+
33.34
|
|
636
|
+
]
|
|
637
|
+
}
|
|
638
|
+
];
|
|
639
|
+
const WIDTH_TOLERANCE = 2;
|
|
640
|
+
const COLUMN_COUNT = {
|
|
641
|
+
"1": 1,
|
|
642
|
+
"2": 2,
|
|
643
|
+
"3": 3,
|
|
644
|
+
"2-1": 2,
|
|
645
|
+
"1-2": 2
|
|
646
|
+
};
|
|
647
|
+
function columnPixels(layout, container) {
|
|
648
|
+
switch (layout) {
|
|
649
|
+
case "2": return [container / 2, container / 2];
|
|
650
|
+
case "3": return [
|
|
651
|
+
container / 3,
|
|
652
|
+
container / 3,
|
|
653
|
+
container / 3
|
|
654
|
+
];
|
|
655
|
+
case "1-2": return [container / 3, container * 2 / 3];
|
|
656
|
+
case "2-1": return [container * 2 / 3, container / 3];
|
|
657
|
+
default: return [container];
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Resolve column widths to one of the five layouts `ColumnLayout` allows.
|
|
662
|
+
*
|
|
663
|
+
* `exact: false` means the caller reports `approximated` — Topol permits any
|
|
664
|
+
* column count at any width and this union permits five shapes, so this is the
|
|
665
|
+
* importer's main irreducible loss.
|
|
666
|
+
*/
|
|
667
|
+
function matchColumnLayout(percents) {
|
|
668
|
+
const count = percents.length;
|
|
669
|
+
if (count === 0) return {
|
|
670
|
+
layout: "1",
|
|
671
|
+
exact: true
|
|
672
|
+
};
|
|
673
|
+
if (percents.every((p) => p === null)) {
|
|
674
|
+
if (count === 1) return {
|
|
675
|
+
layout: "1",
|
|
676
|
+
exact: true
|
|
677
|
+
};
|
|
678
|
+
if (count === 2) return {
|
|
679
|
+
layout: "2",
|
|
680
|
+
exact: true
|
|
681
|
+
};
|
|
682
|
+
if (count === 3) return {
|
|
683
|
+
layout: "3",
|
|
684
|
+
exact: true
|
|
685
|
+
};
|
|
686
|
+
return {
|
|
687
|
+
layout: "3",
|
|
688
|
+
exact: false
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const known = percents.filter((p) => p !== null);
|
|
692
|
+
const unsetCount = count - known.length;
|
|
693
|
+
const remainder = unsetCount === 0 ? 0 : Math.max(0, 100 - known.reduce((a, b) => a + b, 0)) / unsetCount;
|
|
694
|
+
const resolved = percents.map((p) => p ?? remainder);
|
|
695
|
+
const sameCount = LAYOUT_SHAPES.filter((s) => s.percents.length === count);
|
|
696
|
+
for (const shape of sameCount) if (shape.percents.every((want, i) => Math.abs(want - resolved[i]) <= WIDTH_TOLERANCE)) return {
|
|
697
|
+
layout: shape.layout,
|
|
698
|
+
exact: true
|
|
699
|
+
};
|
|
700
|
+
const candidates = sameCount.length > 0 ? sameCount : LAYOUT_SHAPES.filter((s) => s.layout === "3");
|
|
701
|
+
let best = candidates[0];
|
|
702
|
+
let bestError = Infinity;
|
|
703
|
+
for (const shape of candidates) {
|
|
704
|
+
const error = shape.percents.reduce((sum, want, i) => sum + Math.abs(want - (resolved[i] ?? 0)), 0);
|
|
705
|
+
if (error < bestError) {
|
|
706
|
+
bestError = error;
|
|
707
|
+
best = shape;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
layout: best.layout,
|
|
712
|
+
exact: false
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function convertColumnChildren(column, ctx, entries) {
|
|
716
|
+
const blocks = [];
|
|
717
|
+
for (const child of column.children ?? []) {
|
|
718
|
+
const converted = child.tagName === "mj-social" ? convertSocial(child, ctx) : convertLeaf(child, ctx);
|
|
719
|
+
if (!converted) continue;
|
|
720
|
+
entries.push(converted.entry);
|
|
721
|
+
if (converted.block) blocks.push(converted.block);
|
|
722
|
+
}
|
|
723
|
+
return blocks;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Build a `SectionBlock` from an `mj-section`. Returns an array so the caller
|
|
727
|
+
* can treat an empty section (no columns) as producing nothing.
|
|
728
|
+
*
|
|
729
|
+
* Reads `attributes` only. The node's top-level camelCase keys
|
|
730
|
+
* (`backgroundColor`, `paddingTop`, …) are stale editor state that disagrees
|
|
731
|
+
* with `attributes` in every measured case.
|
|
732
|
+
*/
|
|
733
|
+
function buildSection(node, ctx, entries) {
|
|
734
|
+
const columns = (node.children ?? []).filter((c) => c.tagName === "mj-column");
|
|
735
|
+
if (columns.length === 0) return [];
|
|
736
|
+
const rawWidths = columns.map((c) => attr(c, "width"));
|
|
737
|
+
const { layout, exact } = matchColumnLayout(rawWidths.map((w) => parsePercent(w)));
|
|
738
|
+
const slots = COLUMN_COUNT[layout];
|
|
739
|
+
const pixels = columnPixels(layout, ctx.style.settings.width);
|
|
740
|
+
const children = Array.from({ length: slots }, () => []);
|
|
741
|
+
entries.push({
|
|
742
|
+
sourceTag: "mj-section",
|
|
743
|
+
templaticalBlockType: "section",
|
|
744
|
+
status: exact ? "converted" : "approximated",
|
|
745
|
+
...exact ? {} : { note: `Column widths ${rawWidths.map((w) => w ?? "auto").join(", ")} have no exact Templatical layout; resolved to "${layout}".` }
|
|
746
|
+
});
|
|
747
|
+
columns.forEach((column, index) => {
|
|
748
|
+
const slot = Math.min(index, slots - 1);
|
|
749
|
+
const columnCtx = {
|
|
750
|
+
...ctx,
|
|
751
|
+
columnWidth: Math.round(pixels[slot] ?? ctx.style.settings.width)
|
|
752
|
+
};
|
|
753
|
+
children[slot].push(...convertColumnChildren(column, columnCtx, entries));
|
|
754
|
+
});
|
|
755
|
+
const backgroundColor = parseColor(attr(node, "background-color"));
|
|
756
|
+
return [createSectionBlock({
|
|
757
|
+
columns: layout,
|
|
758
|
+
children,
|
|
759
|
+
styles: {
|
|
760
|
+
padding: parsePadding(node),
|
|
761
|
+
...backgroundColor ? { backgroundColor } : {}
|
|
762
|
+
}
|
|
763
|
+
})];
|
|
764
|
+
}
|
|
765
|
+
//#endregion
|
|
766
|
+
//#region src/converter.ts
|
|
767
|
+
const ROOT_TAG = "mj-global-style";
|
|
768
|
+
const INVALID_DESIGN_MESSAGE = "Invalid Topol template: expected the design JSON object — Topol hands you this directly from its editor's onSave callback, or under \"definition\" (template API) or \"json\" (predefined-templates API) in its API response.";
|
|
769
|
+
const EMPTY_DESIGN_WARNING = "No convertible content was found in the Topol design. Check that the mj-container holds at least one mj-section.";
|
|
770
|
+
/**
|
|
771
|
+
* Convert a Topol.io design into a Templatical template.
|
|
772
|
+
*
|
|
773
|
+
* The input is the design object itself — the node whose `tagName` is
|
|
774
|
+
* `mj-global-style`, not a whole API response. Topol's editor hands it to
|
|
775
|
+
* you directly as the first argument to `onSave`; its REST APIs wrap it
|
|
776
|
+
* instead, under `definition` on the template-retrieval endpoint or `json`
|
|
777
|
+
* (alongside `html`) on the predefined-templates endpoint. That is
|
|
778
|
+
* documented rather than sniffed: guessing which field holds the design
|
|
779
|
+
* risks importing an envelope's `html`, which belongs to a different
|
|
780
|
+
* package entirely.
|
|
781
|
+
*
|
|
782
|
+
* @example
|
|
783
|
+
* ```ts
|
|
784
|
+
* const { content, report } = convertTopolTemplate(design);
|
|
785
|
+
* const editor = init({ container: '#editor', content });
|
|
786
|
+
* console.log(report.summary);
|
|
787
|
+
* ```
|
|
788
|
+
*/
|
|
789
|
+
function convertTopolTemplate(design) {
|
|
790
|
+
const root = typeof design === "string" ? safeParse(design) : design;
|
|
791
|
+
if (typeof root !== "object" || root === null || Array.isArray(root)) throw new Error(INVALID_DESIGN_MESSAGE);
|
|
792
|
+
const topolDesign = root;
|
|
793
|
+
if (topolDesign.tagName !== ROOT_TAG) {
|
|
794
|
+
if ("json" in topolDesign || "definition" in topolDesign) throw new Error(INVALID_DESIGN_MESSAGE);
|
|
795
|
+
throw new Error(`Invalid Topol template: expected a root node with tagName "${ROOT_TAG}".`);
|
|
796
|
+
}
|
|
797
|
+
const container = (topolDesign.children ?? []).find((child) => child.tagName === "mj-container");
|
|
798
|
+
const entries = [];
|
|
799
|
+
const warnings = [];
|
|
800
|
+
const blocks = [];
|
|
801
|
+
const style = readGlobalStyle(topolDesign, container, warnings);
|
|
802
|
+
const ctx = {
|
|
803
|
+
style,
|
|
804
|
+
columnWidth: style.settings.width
|
|
805
|
+
};
|
|
806
|
+
for (const child of container?.children ?? []) {
|
|
807
|
+
if (child.tagName !== "mj-section") continue;
|
|
808
|
+
blocks.push(...buildSection(child, ctx, entries));
|
|
809
|
+
}
|
|
810
|
+
if (blocks.length === 0) warnings.push(EMPTY_DESIGN_WARNING);
|
|
811
|
+
return {
|
|
812
|
+
content: {
|
|
813
|
+
...createDefaultTemplateContent(),
|
|
814
|
+
blocks,
|
|
815
|
+
settings: style.settings
|
|
816
|
+
},
|
|
817
|
+
report: {
|
|
818
|
+
entries,
|
|
819
|
+
warnings,
|
|
820
|
+
summary: {
|
|
821
|
+
total: entries.length,
|
|
822
|
+
converted: entries.filter((e) => e.status === "converted").length,
|
|
823
|
+
approximated: entries.filter((e) => e.status === "approximated").length,
|
|
824
|
+
htmlFallback: entries.filter((e) => e.status === "html-fallback").length,
|
|
825
|
+
skipped: entries.filter((e) => e.status === "skipped").length
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function safeParse(source) {
|
|
831
|
+
try {
|
|
832
|
+
return JSON.parse(source);
|
|
833
|
+
} catch {
|
|
834
|
+
throw new Error(INVALID_DESIGN_MESSAGE);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
//#endregion
|
|
838
|
+
export { convertTopolTemplate };
|
|
839
|
+
|
|
840
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/attribute-parser.ts","../src/global-style.ts","../src/content-inference.ts","../src/block-mapper.ts","../src/social-mapper.ts","../src/section-builder.ts","../src/converter.ts"],"sourcesContent":["import type { SpacingValue } from \"@templatical/types\";\nimport type { TopolNode } from \"./types\";\n\n/**\n * Read one attribute as a string, treating Topol's explicit `null` — and an\n * empty string — as absent.\n *\n * Topol writes literal `null` for an unset attribute rather than omitting the\n * key, so a `!== undefined` check would report it as present and write a null\n * into the block. Every optional-field read goes through here.\n */\nexport function attr(node: TopolNode, key: string): string | undefined {\n const raw = node.attributes?.[key];\n if (raw === null || raw === undefined) return undefined;\n if (typeof raw === \"object\") return undefined;\n const value = String(raw);\n return value === \"\" ? undefined : value;\n}\n\n/** Read one attribute as a number, or `undefined` when unset or non-numeric. */\nexport function numAttr(node: TopolNode, key: string): number | undefined {\n const value = attr(node, key);\n if (value === undefined) return undefined;\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\n/**\n * Parse a px-like value into a rounded integer. Returns 0 for a missing value\n * and for units the block model cannot express (em, rem, %).\n *\n * Trims first, then matches with no `\\s*` on either side of the optional\n * `px` unit. A trailing run of spaces can otherwise be claimed by either the\n * surrounding `\\s*` or by backtracking out of the digit/unit match, and a\n * *failing* match (no `px`, no digits — anything not shaped like a length)\n * retries every such split before giving up: `\"0\" + \" \".repeat(n) + \"x\"` is\n * polynomial in `n` against that pattern. Trimming first removes the only\n * whitespace the pattern needs to tolerate, so the match itself has nothing\n * ambiguous left to backtrack over.\n */\nexport function parsePxValue(value: string | number | undefined): number {\n if (value === undefined || value === null || value === \"\") return 0;\n if (typeof value === \"number\") return Math.round(value);\n const trimmed = value.trim();\n const match = /^(-?\\d+(?:\\.\\d+)?)(?:px)?$/.exec(trimmed);\n return match ? Math.round(parseFloat(match[1])) : 0;\n}\n\nconst NAMED_COLORS: Record<string, string> = {\n black: \"#000000\",\n white: \"#ffffff\",\n red: \"#ff0000\",\n green: \"#008000\",\n blue: \"#0000ff\",\n yellow: \"#ffff00\",\n cyan: \"#00ffff\",\n magenta: \"#ff00ff\",\n gray: \"#808080\",\n grey: \"#808080\",\n silver: \"#c0c0c0\",\n maroon: \"#800000\",\n olive: \"#808000\",\n lime: \"#00ff00\",\n aqua: \"#00ffff\",\n teal: \"#008080\",\n navy: \"#000080\",\n fuchsia: \"#ff00ff\",\n purple: \"#800080\",\n orange: \"#ffa500\",\n pink: \"#ffc0cb\",\n};\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n const clamp = (n: number) => Math.max(0, Math.min(255, Math.round(n)));\n const hex = (n: number) => clamp(n).toString(16).padStart(2, \"0\");\n return `#${hex(r)}${hex(g)}${hex(b)}`;\n}\n\n/**\n * Normalize a colour to 6-digit lowercase hex.\n *\n * Returns `\"\"` for transparent/inherit/none and for anything unrecognised —\n * the empty string is the block model's \"unset\", which the colour pickers\n * clear to, so it is meaningfully different from returning a default.\n */\nexport function parseColor(value: string | undefined): string {\n if (!value) return \"\";\n const trimmed = value.trim().toLowerCase();\n if (trimmed === \"transparent\" || trimmed === \"inherit\" || trimmed === \"none\")\n return \"\";\n if (/^#[0-9a-f]{6}$/.test(trimmed)) return trimmed;\n if (/^#[0-9a-f]{3}$/.test(trimmed)) {\n const [, r, g, b] = trimmed;\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n const rgb = trimmed.match(\n /^rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*(?:,\\s*[\\d.]+\\s*)?\\)$/,\n );\n if (rgb) return rgbToHex(+rgb[1], +rgb[2], +rgb[3]);\n return NAMED_COLORS[trimmed] ?? \"\";\n}\n\nfunction expandShorthand(value: string | undefined): SpacingValue {\n if (!value) return { top: 0, right: 0, bottom: 0, left: 0 };\n const v = value\n .trim()\n .split(/\\s+/)\n .map((p) => parsePxValue(p));\n switch (v.length) {\n case 1:\n return { top: v[0], right: v[0], bottom: v[0], left: v[0] };\n case 2:\n return { top: v[0], right: v[1], bottom: v[0], left: v[1] };\n case 3:\n return { top: v[0], right: v[1], bottom: v[2], left: v[1] };\n default:\n return { top: v[0], right: v[1], bottom: v[2], left: v[3] };\n }\n}\n\n/**\n * Read a node's padding, handling both forms Topol emits: a shorthand string\n * (`\"9px 9px 9px 9px\"`, used by most leaves) and four separate bare-number\n * keys (`padding-top: 24`, used by `mj-divider`). A separate key wins over the\n * shorthand for its own side.\n */\nexport function parsePadding(node: TopolNode): SpacingValue {\n const base = expandShorthand(attr(node, \"padding\"));\n const side = (key: string, fallback: number) => {\n const value = attr(node, key);\n return value === undefined ? fallback : parsePxValue(value);\n };\n return {\n top: side(\"padding-top\", base.top),\n right: side(\"padding-right\", base.right),\n bottom: side(\"padding-bottom\", base.bottom),\n left: side(\"padding-left\", base.left),\n };\n}\n\n/**\n * Read a percentage. Returns `null` when the value is not a percentage, so a\n * caller can tell \"no width given\" from \"0%\" — column matching depends on that\n * distinction.\n */\nexport function parsePercent(\n value: string | number | undefined,\n): number | null {\n if (value === undefined || value === null || value === \"\") return null;\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n const match = value.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*%$/);\n return match ? parseFloat(match[1]) : null;\n}\n\n/** Strip quotes and return the first family in a stack. */\nexport function parseFontFamily(value: string | undefined): string {\n if (!value) return \"\";\n return value\n .split(\",\")[0]\n .trim()\n .replace(/^['\"]|['\"]$/g, \"\");\n}\n\n/** Narrow an alignment to the three the block model accepts. */\nexport function parseAlignment(\n value: string | undefined,\n fallback: \"left\" | \"center\" | \"right\" = \"left\",\n): \"left\" | \"center\" | \"right\" {\n const v = (value ?? \"\").trim().toLowerCase();\n return v === \"left\" || v === \"center\" || v === \"right\" ? v : fallback;\n}\n\n/** Narrow a border style to the three `DividerBlock.lineStyle` accepts. */\nexport function parseBorderStyle(\n value: string | undefined,\n): \"solid\" | \"dashed\" | \"dotted\" {\n const v = (value ?? \"\").trim().toLowerCase();\n return v === \"dashed\" || v === \"dotted\" ? v : \"solid\";\n}\n","import { DEFAULT_TEMPLATE_DEFAULTS } from \"@templatical/types\";\nimport type { TemplateContent, TemplateSettings } from \"@templatical/types\";\nimport {\n attr,\n parseColor,\n parseFontFamily,\n parsePxValue,\n} from \"./attribute-parser\";\nimport type { TopolDesign, TopolNode } from \"./types\";\n\n/**\n * `DEFAULT_TEMPLATE_DEFAULTS` is typed `Partial<TemplateSettings>` so a\n * consumer can override any subset, but its own literal always sets the six\n * required fields. Narrowing once here is what lets each read come out as\n * `number`/`string`/`boolean` rather than `| undefined`. A plain typed\n * assignment cannot substitute: `Partial<T>` is not assignable to\n * `Required<Pick<T, …>>`, so `tsc` rejects it outright.\n */\nconst REQUIRED_DEFAULTS = DEFAULT_TEMPLATE_DEFAULTS as Required<\n Pick<\n TemplateSettings,\n | \"width\"\n | \"backgroundColor\"\n | \"textColor\"\n | \"linkUnderline\"\n | \"fontFamily\"\n | \"locale\"\n >\n>;\n\nexport interface GlobalStyle {\n settings: TemplateContent[\"settings\"];\n /** From nested objects like `\"mj-text\": { … }`. */\n tagDefaults: Record<string, Record<string, string>>;\n /** From flat `<selector>:<property>` keys; the bare selector is `\"\"`. */\n selectorDefaults: Record<string, Record<string, string>>;\n}\n\n/** A per-tag default, or undefined when the design sets none. */\nexport function tagDefault(\n style: GlobalStyle,\n tag: string,\n key: string,\n): string | undefined {\n return style.tagDefaults[tag]?.[key];\n}\n\n/** A per-selector default (`\"\"` is the document default), or undefined. */\nexport function selectorDefault(\n style: GlobalStyle,\n selector: string,\n key: string,\n): string | undefined {\n return style.selectorDefaults[selector]?.[key];\n}\n\n/**\n * Split the root's `attributes` into settings, per-tag defaults and\n * per-selector defaults.\n *\n * The root mixes two idioms: `<selector>:<property>` flat keys (`:color`,\n * `a:color`, `h1:font-family`) and `<tag>: { … }` nested objects, which are\n * the analogue of MJML's `mj-attributes`.\n */\nexport function readGlobalStyle(\n root: TopolDesign,\n container: TopolNode | undefined,\n warnings: string[],\n): GlobalStyle {\n const tagDefaults: Record<string, Record<string, string>> = Object.create(\n null,\n );\n const selectorDefaults: Record<\n string,\n Record<string, string>\n > = Object.create(null);\n\n for (const [key, value] of Object.entries(root.attributes ?? {})) {\n if (value !== null && typeof value === \"object\") {\n const bucket = (tagDefaults[key] ??= Object.create(null));\n for (const [k, v] of Object.entries(value)) {\n if (v !== null && v !== undefined) bucket[k] = String(v);\n }\n continue;\n }\n\n const colon = key.indexOf(\":\");\n // No colon means a root-level setting rather than a selector default —\n // `containerWidth` and `fonts` are the two real cases, and both are read\n // directly by name below instead of through the caches built here.\n if (colon === -1) continue;\n const selector = key.slice(0, colon);\n const property = key.slice(colon + 1);\n if (value === null || value === undefined) continue;\n const bucket = (selectorDefaults[selector] ??= Object.create(null));\n bucket[property] = String(value);\n }\n\n // containerWidth is authored only here, on the root. It is also denormalised\n // onto every leaf as the enclosing column's computed width (600/300/150 in\n // one template), so no other module may read it.\n const width =\n parsePxValue(attr(root, \"containerWidth\")) || REQUIRED_DEFAULTS.width;\n\n const backgroundColor =\n (container ? parseColor(attr(container, \"background-color\")) : \"\") ||\n REQUIRED_DEFAULTS.backgroundColor;\n\n const textColor =\n parseColor(selectorDefaults[\"\"]?.color) || REQUIRED_DEFAULTS.textColor;\n\n const fontFamily =\n parseFontFamily(selectorDefaults[\"\"]?.[\"font-family\"]) ||\n parseFontFamily(attr(root, \"fonts\")) ||\n REQUIRED_DEFAULTS.fontFamily;\n\n const linkColor = parseColor(selectorDefaults[\"a\"]?.color);\n\n const lineHeight = selectorDefaults[\"\"]?.[\"line-height\"];\n if (lineHeight !== undefined) {\n warnings.push(\n `Dropped the document line-height (${lineHeight}) — Templatical has no document-level line-height setting.`,\n );\n }\n\n const settings: TemplateContent[\"settings\"] = {\n width,\n backgroundColor,\n textColor,\n linkUnderline: REQUIRED_DEFAULTS.linkUnderline,\n fontFamily,\n locale: REQUIRED_DEFAULTS.locale,\n ...(linkColor ? { linkColor } : {}),\n };\n\n return { settings, tagDefaults, selectorDefaults };\n}\n","import { findOpenTagEnd } from \"@templatical/types\";\nimport type { HeadingLevel } from \"@templatical/types\";\n\nexport type InferredText =\n | { kind: \"title\"; level: HeadingLevel; inner: string; clampedFrom?: number }\n | { kind: \"paragraph\"; html: string };\n\n/**\n * A single heading element wrapping the whole content, captured with its level\n * and inner markup. Anchored at both ends so a heading with a sibling does not\n * match — that case is a paragraph.\n */\nconst SOLE_HEADING = /^<h([1-6])(?:\\s[^>]*)?>([\\s\\S]*)<\\/h\\1>$/i;\n\nconst BLOCK_ROOT = /^<(p|h[1-6]|ul|ol|blockquote|div|table)\\b/i;\n\n/**\n * Resolve a Topol `content` string to a Title or a Paragraph.\n *\n * Topol has no menu or table construct, so this is a two-way choice — the MJML\n * importer needs four arms because `mj-text` is the output of four different\n * block renderers there. Paragraph is the terminal arm and always reachable,\n * so this function is total.\n */\nexport function inferTextBlock(content: string | undefined): InferredText {\n const trimmed = (content ?? \"\").trim();\n if (trimmed === \"\") return { kind: \"paragraph\", html: \"<p></p>\" };\n\n const heading = trimmed.match(SOLE_HEADING);\n if (heading) {\n const source = Number(heading[1]);\n const level = Math.min(source, 4) as HeadingLevel;\n return {\n kind: \"title\",\n level,\n inner: heading[2].trim(),\n ...(source > 4 ? { clampedFrom: source } : {}),\n };\n }\n\n return {\n kind: \"paragraph\",\n html: BLOCK_ROOT.test(trimmed) ? trimmed : `<p>${trimmed}</p>`,\n };\n}\n\nconst ENTITIES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n \""\": '\"',\n \"'\": \"'\",\n \"'\": \"'\",\n \" \": \" \",\n};\n\n/**\n * Remove every `<tag>` from `html`, keeping the text between them.\n *\n * A single-pass linear scan rather than `/<[^>]*>/g`: that regex is\n * polynomial over input with many `<` starts and no closing `>`, since\n * `[^>]*` backtracks at every one of them — `\"<\".repeat(n)` is quadratic in\n * `n`. `findOpenTagEnd` (from `@templatical/types`) resolves each `<` with a\n * bounded, quote-aware forward scan instead, so the walk stays O(n): every\n * character is visited at most once by `indexOf` and once by\n * `findOpenTagEnd`, because the cursor only moves forward and an\n * unterminated tag ends the walk rather than retrying from inside it. On\n * that unterminated case the remainder of the string — the dangling `<`\n * included — is emitted as-is, matching what `/<[^>]*>/g` does with\n * malformed markup: it can never complete a match without a `>`, so the\n * text is left untouched from that point on.\n */\nfunction removeTags(html: string): string {\n let out = \"\";\n let i = 0;\n while (i < html.length) {\n const open = html.indexOf(\"<\", i);\n if (open === -1) {\n out += html.substring(i);\n break;\n }\n out += html.substring(i, open);\n const tagEnd = findOpenTagEnd(html, open + 1);\n if (tagEnd === -1) {\n out += html.substring(open);\n break;\n }\n i = tagEnd + 1;\n }\n return out;\n}\n\n/**\n * The visible text of a markup fragment, for a button label — `ButtonBlock.text`\n * is a plain string, so the markup Topol stores in `content` cannot travel.\n */\nexport function stripTags(html: string | undefined): string {\n if (!html) return \"\";\n const text = removeTags(html);\n const decoded = text.replace(\n /&(?:amp|lt|gt|quot|#39|apos|nbsp);/g,\n (m) => ENTITIES[m] ?? m,\n );\n return decoded.replace(/\\s+/g, \" \").trim();\n}\n","import {\n createButtonBlock,\n createDividerBlock,\n createHtmlBlock,\n createImageBlock,\n createParagraphBlock,\n createSpacerBlock,\n createTitleBlock,\n} from \"@templatical/types\";\nimport type { Block, BlockStyles } from \"@templatical/types\";\nimport {\n attr,\n numAttr,\n parseAlignment,\n parseBorderStyle,\n parseColor,\n parseFontFamily,\n parsePadding,\n parsePxValue,\n} from \"./attribute-parser\";\nimport { inferTextBlock, stripTags } from \"./content-inference\";\nimport { selectorDefault, tagDefault, type GlobalStyle } from \"./global-style\";\nimport type { ImportReportEntry, TopolNode } from \"./types\";\n\nexport interface MapContext {\n style: GlobalStyle;\n /** The rendered width of the column holding this node, for `width: \"full\"`. */\n columnWidth: number;\n}\n\nexport interface Converted {\n block: Block | null;\n entry: ImportReportEntry;\n}\n\n/**\n * Read a node's own attribute, falling back to the design's per-tag then\n * per-selector default. Returns undefined when nothing sets it, so the caller\n * can omit the field and let the block factory's default stand.\n */\nfunction resolved(\n node: TopolNode,\n key: string,\n ctx: MapContext,\n selector?: string,\n): string | undefined {\n return (\n attr(node, key) ??\n tagDefault(ctx.style, node.tagName, key) ??\n (selector === undefined\n ? undefined\n : selectorDefault(ctx.style, selector, key))\n );\n}\n\nexport function baseStyles(node: TopolNode): { styles: BlockStyles } {\n const backgroundColor = parseColor(attr(node, \"background-color\"));\n return {\n styles: {\n padding: parsePadding(node),\n ...(backgroundColor ? { backgroundColor } : {}),\n },\n };\n}\n\nfunction convertText(node: TopolNode, ctx: MapContext): Converted {\n const inferred = inferTextBlock(node.content);\n\n if (inferred.kind === \"paragraph\") {\n return {\n block: createParagraphBlock({\n content: inferred.html,\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"paragraph\",\n status: \"converted\",\n },\n };\n }\n\n const selector = `h${inferred.level}`;\n const color = parseColor(resolved(node, \"color\", ctx, selector));\n const fontFamily = parseFontFamily(\n resolved(node, \"font-family\", ctx, selector),\n );\n\n const block = createTitleBlock({\n content: inferred.inner,\n level: inferred.level,\n textAlign: parseAlignment(attr(node, \"align\"), \"left\"),\n ...(color ? { color } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...baseStyles(node),\n });\n\n return {\n block,\n entry: {\n sourceTag: \"mj-text\",\n templaticalBlockType: \"title\",\n status: inferred.clampedFrom ? \"approximated\" : \"converted\",\n ...(inferred.clampedFrom\n ? {\n note: `Heading level h${inferred.clampedFrom} clamped to 4 — Templatical titles support h1-h4.`,\n }\n : {}),\n },\n };\n}\n\nfunction convertButton(node: TopolNode, ctx: MapContext): Converted | null {\n const text = stripTags(node.content);\n if (!text) return null;\n\n const backgroundColor = parseColor(\n resolved(node, \"background-color\", ctx, \"button\"),\n );\n const textColor = parseColor(resolved(node, \"color\", ctx, \"button\"));\n const fontFamily = parseFontFamily(\n resolved(node, \"font-family\", ctx, \"button\"),\n );\n const fontSizeRaw = resolved(node, \"font-size\", ctx);\n const radiusRaw = resolved(node, \"border-radius\", ctx);\n\n return {\n block: createButtonBlock({\n text,\n url: attr(node, \"href\") ?? \"\",\n ...(backgroundColor ? { backgroundColor } : {}),\n ...(textColor ? { textColor } : {}),\n ...(fontFamily ? { fontFamily } : {}),\n ...(fontSizeRaw === undefined\n ? {}\n : { fontSize: parsePxValue(fontSizeRaw) }),\n ...(radiusRaw === undefined\n ? {}\n : { borderRadius: parsePxValue(radiusRaw) }),\n align: parseAlignment(attr(node, \"align\"), \"center\"),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: node.tagName,\n templaticalBlockType: \"button\",\n status: \"converted\",\n },\n };\n}\n\nfunction convertImage(\n node: TopolNode,\n ctx: MapContext,\n sourceTag: string,\n): Converted | null {\n const src = attr(node, \"src\");\n if (!src) return null;\n\n const widthPercent = numAttr(node, \"widthPercent\");\n const pxWidth = numAttr(node, \"width\");\n const href = attr(node, \"href\");\n\n const width =\n widthPercent === 100 || pxWidth === undefined || pxWidth >= ctx.columnWidth\n ? (\"full\" as const)\n : Math.round(pxWidth);\n\n const isGif = sourceTag === \"mj-gif\";\n\n return {\n block: createImageBlock({\n src,\n alt: attr(node, \"alt\") ?? \"\",\n width,\n align: parseAlignment(attr(node, \"align\"), \"center\"),\n ...(href ? { linkUrl: href } : {}),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag,\n templaticalBlockType: \"image\",\n status: isGif ? \"approximated\" : \"converted\",\n ...(isGif\n ? {\n note: \"<mj-gif> imported as an image block; Templatical has no dedicated GIF block.\",\n }\n : {}),\n },\n };\n}\n\nfunction convertSpacer(node: TopolNode): Converted {\n const height = attr(node, \"height\");\n return {\n block: createSpacerBlock({\n ...(height === undefined ? {} : { height: parsePxValue(height) }),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-spacer\",\n templaticalBlockType: \"spacer\",\n status: \"converted\",\n },\n };\n}\n\nfunction convertDivider(node: TopolNode): Converted {\n const color = parseColor(attr(node, \"border-color\"));\n const thickness = attr(node, \"border-width\");\n return {\n block: createDividerBlock({\n lineStyle: parseBorderStyle(attr(node, \"border-style\")),\n ...(color ? { color } : {}),\n ...(thickness === undefined\n ? {}\n : { thickness: parsePxValue(thickness) }),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-divider\",\n templaticalBlockType: \"divider\",\n status: \"converted\",\n },\n };\n}\n\n/**\n * Convert one leaf node to a block.\n *\n * Returns `null` for a node that produces nothing *and* warrants no report\n * entry — an image with no `src`, a button with no label. `mj-social` is not\n * handled here; the converter routes it to `convertSocial`.\n */\nexport function convertLeaf(\n node: TopolNode,\n ctx: MapContext,\n): Converted | null {\n switch (node.tagName) {\n case \"mj-text\":\n return convertText(node, ctx);\n case \"mj-button\":\n return convertButton(node, ctx);\n case \"mj-image\":\n return convertImage(node, ctx, \"mj-image\");\n case \"mj-gif\":\n return convertImage(node, ctx, \"mj-gif\");\n case \"mj-spacer\":\n return convertSpacer(node);\n case \"mj-divider\":\n return convertDivider(node);\n default:\n return {\n block: createHtmlBlock({\n content: JSON.stringify(node, null, 2),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: node.tagName,\n templaticalBlockType: \"html\",\n status: \"html-fallback\",\n note: `<${node.tagName}> has no Templatical block equivalent; the original node is preserved as JSON.`,\n },\n };\n }\n}\n","import { createSocialIconsBlock, generateId } from \"@templatical/types\";\nimport type {\n SocialIcon,\n SocialIconSize,\n SocialIconStyle,\n SocialPlatform,\n} from \"@templatical/types\";\nimport { attr, parseAlignment, parsePxValue } from \"./attribute-parser\";\nimport { baseStyles, type Converted, type MapContext } from \"./block-mapper\";\nimport type { TopolNode } from \"./types\";\n\n/**\n * Exhaustive over `SocialPlatform` on purpose: adding a member to that union\n * without adding it here is a compile error, so this cannot silently fall back\n * to \"website\" for a platform the block model gained.\n */\nconst KNOWN_PLATFORMS: Record<SocialPlatform, true> = {\n facebook: true,\n twitter: true,\n instagram: true,\n linkedin: true,\n youtube: true,\n tiktok: true,\n pinterest: true,\n email: true,\n whatsapp: true,\n telegram: true,\n discord: true,\n snapchat: true,\n reddit: true,\n github: true,\n dribbble: true,\n behance: true,\n website: true,\n};\n\nconst ALIASES: Record<string, SocialPlatform> = {\n x: \"twitter\",\n \"x-twitter\": \"twitter\",\n};\n\nconst ICON_SIZES: Array<[number, SocialIconSize]> = [\n [24, \"small\"],\n [32, \"medium\"],\n [48, \"large\"],\n];\n\nconst KNOWN_STYLES = new Set<string>([\n \"solid\",\n \"outlined\",\n \"rounded\",\n \"square\",\n \"circle\",\n]);\n\n// Topol ships icon sets whose folder name is not a Templatical style name.\n// `outlinedbw` is the outlined set in black and white; Templatical has no\n// monochrome variant, so `outlined` is the closest true answer — mapping it to\n// the `solid` fallback instead would import an outlined icon set as filled.\n// Measured: both `/social-icos/outlined/` and `/social-icos/outlinedbw/` appear\n// across the reference exports.\nconst STYLE_ALIASES: Record<string, SocialIconStyle> = {\n outlinedbw: \"outlined\",\n};\n\nfunction normalizePlatform(raw: string): SocialPlatform | null {\n const cleaned = raw.trim().toLowerCase();\n if (!cleaned) return null;\n if (ALIASES[cleaned]) return ALIASES[cleaned];\n return cleaned in KNOWN_PLATFORMS ? (cleaned as SocialPlatform) : null;\n}\n\nfunction nearestSize(px: number): { size: SocialIconSize; exact: boolean } {\n let best = ICON_SIZES[1];\n let gap = Infinity;\n for (const candidate of ICON_SIZES) {\n const d = Math.abs(candidate[0] - px);\n if (d < gap) {\n gap = d;\n best = candidate;\n }\n }\n return { size: best[1], exact: gap === 0 };\n}\n\n/** The style segment of `https://…/social-icos/<style>/`, when it names one. */\nfunction styleFromBaseUrl(\n baseUrl: string | undefined,\n): SocialIconStyle | undefined {\n if (!baseUrl) return undefined;\n const segment = baseUrl.split(\"?\")[0].split(\"/\").filter(Boolean).at(-1) ?? \"\";\n if (KNOWN_STYLES.has(segment)) return segment as SocialIconStyle;\n return STYLE_ALIASES[segment];\n}\n\n/**\n * Convert Topol's MJML v3 social block.\n *\n * `display` decides which icons render and in what order; a platform with\n * `*-href` set but absent from `display` was not shown in the email and is\n * not imported.\n */\nexport function convertSocial(\n node: TopolNode,\n _ctx: MapContext,\n): Converted | null {\n const display = attr(node, \"display\");\n if (!display) return null;\n\n const notes: string[] = [];\n const icons: SocialIcon[] = [];\n\n for (const token of display.trim().split(/\\s+/)) {\n const rawName = token.split(\":\")[0];\n if (!rawName) continue;\n\n const url = attr(node, `${rawName}-href`);\n if (!url) continue;\n\n const platform = normalizePlatform(rawName);\n if (!platform) {\n notes.push(\n `Unrecognised social platform \"${rawName}\" mapped to \"website\".`,\n );\n }\n\n icons.push({ id: generateId(), platform: platform ?? \"website\", url });\n }\n\n if (icons.length === 0) return null;\n\n const iconStyle = styleFromBaseUrl(attr(node, \"base-url\"));\n\n const declaredSize = attr(node, \"icon-size\");\n let iconSize: SocialIconSize | undefined;\n if (declaredSize !== undefined) {\n const px = parsePxValue(declaredSize);\n const resolved = nearestSize(px);\n iconSize = resolved.size;\n if (!resolved.exact) {\n notes.push(\n `Icon size ${declaredSize} is not one of 24/32/48px; resolved to \"${resolved.size}\".`,\n );\n }\n }\n\n return {\n block: createSocialIconsBlock({\n icons,\n align: parseAlignment(attr(node, \"align\"), \"center\"),\n ...(iconSize ? { iconSize } : {}),\n ...(iconStyle ? { iconStyle } : {}),\n ...baseStyles(node),\n }),\n entry: {\n sourceTag: \"mj-social\",\n templaticalBlockType: \"social\",\n status: notes.length > 0 ? \"approximated\" : \"converted\",\n ...(notes.length > 0 ? { note: notes.join(\" \") } : {}),\n },\n };\n}\n","import { createSectionBlock } from \"@templatical/types\";\nimport type { Block, ColumnLayout } from \"@templatical/types\";\nimport {\n attr,\n parseColor,\n parsePadding,\n parsePercent,\n} from \"./attribute-parser\";\nimport { convertLeaf, type MapContext } from \"./block-mapper\";\nimport { convertSocial } from \"./social-mapper\";\nimport type { ImportReportEntry, TopolNode } from \"./types\";\n\nconst LAYOUT_SHAPES: Array<{ layout: ColumnLayout; percents: number[] }> = [\n { layout: \"1\", percents: [100] },\n { layout: \"2\", percents: [50, 50] },\n { layout: \"1-2\", percents: [33.33, 66.67] },\n { layout: \"2-1\", percents: [66.67, 33.33] },\n { layout: \"3\", percents: [33.33, 33.33, 33.34] },\n];\n\nconst WIDTH_TOLERANCE = 2;\n\nconst COLUMN_COUNT: Record<ColumnLayout, number> = {\n \"1\": 1,\n \"2\": 2,\n \"3\": 3,\n \"2-1\": 2,\n \"1-2\": 2,\n};\n\nfunction columnPixels(layout: ColumnLayout, container: number): number[] {\n switch (layout) {\n case \"2\":\n return [container / 2, container / 2];\n case \"3\":\n return [container / 3, container / 3, container / 3];\n case \"1-2\":\n return [container / 3, (container * 2) / 3];\n case \"2-1\":\n return [(container * 2) / 3, container / 3];\n default:\n return [container];\n }\n}\n\n/**\n * Resolve column widths to one of the five layouts `ColumnLayout` allows.\n *\n * `exact: false` means the caller reports `approximated` — Topol permits any\n * column count at any width and this union permits five shapes, so this is the\n * importer's main irreducible loss.\n */\nexport function matchColumnLayout(percents: Array<number | null>): {\n layout: ColumnLayout;\n exact: boolean;\n} {\n const count = percents.length;\n if (count === 0) return { layout: \"1\", exact: true };\n\n // All-unset is \"distribute equally\", which is exactly what the n-column\n // layout means — not a missing value to approximate around.\n if (percents.every((p) => p === null)) {\n if (count === 1) return { layout: \"1\", exact: true };\n if (count === 2) return { layout: \"2\", exact: true };\n if (count === 3) return { layout: \"3\", exact: true };\n return { layout: \"3\", exact: false };\n }\n\n const known = percents.filter((p): p is number => p !== null);\n const unsetCount = count - known.length;\n const remainder =\n unsetCount === 0\n ? 0\n : Math.max(0, 100 - known.reduce((a, b) => a + b, 0)) / unsetCount;\n const resolved = percents.map((p) => p ?? remainder);\n\n const sameCount = LAYOUT_SHAPES.filter((s) => s.percents.length === count);\n\n for (const shape of sameCount) {\n if (\n shape.percents.every(\n (want, i) => Math.abs(want - resolved[i]) <= WIDTH_TOLERANCE,\n )\n ) {\n return { layout: shape.layout, exact: true };\n }\n }\n\n const candidates =\n sameCount.length > 0\n ? sameCount\n : LAYOUT_SHAPES.filter((s) => s.layout === \"3\");\n\n let best = candidates[0];\n let bestError = Infinity;\n for (const shape of candidates) {\n const error = shape.percents.reduce(\n (sum, want, i) => sum + Math.abs(want - (resolved[i] ?? 0)),\n 0,\n );\n if (error < bestError) {\n bestError = error;\n best = shape;\n }\n }\n\n return { layout: best.layout, exact: false };\n}\n\nfunction convertColumnChildren(\n column: TopolNode,\n ctx: MapContext,\n entries: ImportReportEntry[],\n): Block[] {\n const blocks: Block[] = [];\n for (const child of column.children ?? []) {\n const converted =\n child.tagName === \"mj-social\"\n ? convertSocial(child, ctx)\n : convertLeaf(child, ctx);\n if (!converted) continue;\n entries.push(converted.entry);\n if (converted.block) blocks.push(converted.block);\n }\n return blocks;\n}\n\n/**\n * Build a `SectionBlock` from an `mj-section`. Returns an array so the caller\n * can treat an empty section (no columns) as producing nothing.\n *\n * Reads `attributes` only. The node's top-level camelCase keys\n * (`backgroundColor`, `paddingTop`, …) are stale editor state that disagrees\n * with `attributes` in every measured case.\n */\nexport function buildSection(\n node: TopolNode,\n ctx: MapContext,\n entries: ImportReportEntry[],\n): Block[] {\n const columns = (node.children ?? []).filter(\n (c) => c.tagName === \"mj-column\",\n );\n if (columns.length === 0) return [];\n\n const rawWidths = columns.map((c) => attr(c, \"width\"));\n const percents = rawWidths.map((w) => parsePercent(w));\n const { layout, exact } = matchColumnLayout(percents);\n\n const slots = COLUMN_COUNT[layout];\n const pixels = columnPixels(layout, ctx.style.settings.width);\n const children: Block[][] = Array.from({ length: slots }, () => []);\n\n entries.push({\n sourceTag: \"mj-section\",\n templaticalBlockType: \"section\",\n status: exact ? \"converted\" : \"approximated\",\n ...(exact\n ? {}\n : {\n note: `Column widths ${rawWidths.map((w) => w ?? \"auto\").join(\", \")} have no exact Templatical layout; resolved to \"${layout}\".`,\n }),\n });\n\n columns.forEach((column, index) => {\n const slot = Math.min(index, slots - 1);\n const columnCtx: MapContext = {\n ...ctx,\n columnWidth: Math.round(pixels[slot] ?? ctx.style.settings.width),\n };\n children[slot].push(...convertColumnChildren(column, columnCtx, entries));\n });\n\n const backgroundColor = parseColor(attr(node, \"background-color\"));\n\n return [\n createSectionBlock({\n columns: layout,\n children,\n styles: {\n padding: parsePadding(node),\n ...(backgroundColor ? { backgroundColor } : {}),\n },\n }),\n ];\n}\n","import { createDefaultTemplateContent } from \"@templatical/types\";\nimport type { Block, TemplateContent } from \"@templatical/types\";\nimport { readGlobalStyle } from \"./global-style\";\nimport { buildSection } from \"./section-builder\";\nimport type { MapContext } from \"./block-mapper\";\nimport type {\n ImportReport,\n ImportReportEntry,\n ImportResult,\n TopolDesign,\n} from \"./types\";\n\nconst ROOT_TAG = \"mj-global-style\";\n\nconst INVALID_DESIGN_MESSAGE =\n 'Invalid Topol template: expected the design JSON object — Topol hands you this directly from its editor\\'s onSave callback, or under \"definition\" (template API) or \"json\" (predefined-templates API) in its API response.';\n\nconst EMPTY_DESIGN_WARNING =\n \"No convertible content was found in the Topol design. Check that the mj-container holds at least one mj-section.\";\n\n/**\n * Convert a Topol.io design into a Templatical template.\n *\n * The input is the design object itself — the node whose `tagName` is\n * `mj-global-style`, not a whole API response. Topol's editor hands it to\n * you directly as the first argument to `onSave`; its REST APIs wrap it\n * instead, under `definition` on the template-retrieval endpoint or `json`\n * (alongside `html`) on the predefined-templates endpoint. That is\n * documented rather than sniffed: guessing which field holds the design\n * risks importing an envelope's `html`, which belongs to a different\n * package entirely.\n *\n * @example\n * ```ts\n * const { content, report } = convertTopolTemplate(design);\n * const editor = init({ container: '#editor', content });\n * console.log(report.summary);\n * ```\n */\nexport function convertTopolTemplate(\n design: TopolDesign | string,\n): ImportResult {\n const root: unknown = typeof design === \"string\" ? safeParse(design) : design;\n\n if (typeof root !== \"object\" || root === null || Array.isArray(root)) {\n throw new Error(INVALID_DESIGN_MESSAGE);\n }\n\n const topolDesign = root as TopolDesign;\n\n if (topolDesign.tagName !== ROOT_TAG) {\n // Topol's REST APIs wrap the design rather than returning it bare: the\n // template-retrieval endpoint uses \"definition\", the predefined/premade\n // templates endpoint uses \"json\" (alongside \"html\"). A response object\n // passed here by mistake still carries one of those keys, so that\n // mistake is named directly instead of falling through to the generic\n // wrong-tagName message below.\n if (\"json\" in topolDesign || \"definition\" in topolDesign) {\n throw new Error(INVALID_DESIGN_MESSAGE);\n }\n throw new Error(\n `Invalid Topol template: expected a root node with tagName \"${ROOT_TAG}\".`,\n );\n }\n\n const container = (topolDesign.children ?? []).find(\n (child) => child.tagName === \"mj-container\",\n );\n\n const entries: ImportReportEntry[] = [];\n const warnings: string[] = [];\n const blocks: Block[] = [];\n\n const style = readGlobalStyle(topolDesign, container, warnings);\n\n const ctx: MapContext = {\n style,\n columnWidth: style.settings.width,\n };\n\n for (const child of container?.children ?? []) {\n if (child.tagName !== \"mj-section\") continue;\n blocks.push(...buildSection(child, ctx, entries));\n }\n\n if (blocks.length === 0) {\n warnings.push(EMPTY_DESIGN_WARNING);\n }\n\n const content: TemplateContent = {\n ...createDefaultTemplateContent(),\n blocks,\n settings: style.settings,\n };\n\n const summary = {\n total: entries.length,\n converted: entries.filter((e) => e.status === \"converted\").length,\n approximated: entries.filter((e) => e.status === \"approximated\").length,\n htmlFallback: entries.filter((e) => e.status === \"html-fallback\").length,\n skipped: entries.filter((e) => e.status === \"skipped\").length,\n };\n\n const report: ImportReport = { entries, warnings, summary };\n\n return { content, report };\n}\n\nfunction safeParse(source: string): unknown {\n try {\n return JSON.parse(source);\n } catch {\n throw new Error(INVALID_DESIGN_MESSAGE);\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@templatical/import-topol",
|
|
3
|
+
"description": "Convert Topol.io email templates to Templatical format",
|
|
4
|
+
"version": "0.32.0",
|
|
5
|
+
"bugs": "https://github.com/templatical/sdk/issues",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@templatical/types": "0.32.0"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"typescript": "^6.0.3",
|
|
11
|
+
"vitest": "^5.0.0"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://templatical.com",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"email",
|
|
25
|
+
"email-template",
|
|
26
|
+
"importer",
|
|
27
|
+
"migration",
|
|
28
|
+
"templatical",
|
|
29
|
+
"topol"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"module": "./dist/index.js",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/templatical/sdk.git",
|
|
39
|
+
"directory": "packages/import-topol"
|
|
40
|
+
},
|
|
41
|
+
"type": "module",
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsdown",
|
|
45
|
+
"test": "vitest run --config vitest.config.ts",
|
|
46
|
+
"typecheck": "tsc --noEmit"
|
|
47
|
+
}
|
|
48
|
+
}
|