@templatical/import-html 0.52.1 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +6 -2
- package/dist/index.js +297 -47
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,10 @@ interface ImportResult {
|
|
|
37
37
|
content: import("@templatical/types").TemplateContent;
|
|
38
38
|
report: ImportReport;
|
|
39
39
|
}
|
|
40
|
+
interface ConvertHtmlOptions {
|
|
41
|
+
/** Used when the source declares no default language. */
|
|
42
|
+
defaultLocale?: string;
|
|
43
|
+
}
|
|
40
44
|
/**
|
|
41
45
|
* Converts an HTML email template to Templatical TemplateContent.
|
|
42
46
|
*
|
|
@@ -60,6 +64,6 @@ interface ImportResult {
|
|
|
60
64
|
* console.log(report.warnings);
|
|
61
65
|
* ```
|
|
62
66
|
*/
|
|
63
|
-
export declare function convertHtmlTemplate(html: string): ImportResult;
|
|
64
|
-
export type { ConversionStatus, ImportReport, ImportReportEntry, ImportResult };
|
|
67
|
+
export declare function convertHtmlTemplate(html: string, options?: ConvertHtmlOptions): ImportResult;
|
|
68
|
+
export type { ConversionStatus, ConvertHtmlOptions, ImportReport, ImportReportEntry, ImportResult };
|
|
65
69
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { load } from "cheerio";
|
|
2
|
-
import { canonicalFontStack, createButtonBlock, createDefaultTemplateContent, createDividerBlock, createHtmlBlock, createImageBlock, createParagraphBlock, createSectionBlock, createSpacerBlock, createTitleBlock } from "@templatical/types";
|
|
2
|
+
import { canonicalFontStack, createButtonBlock, createDefaultTemplateContent, createDividerBlock, createHtmlBlock, createImageBlock, createParagraphBlock, createSectionBlock, createSpacerBlock, createTitleBlock, isLocaleTag } from "@templatical/types";
|
|
3
3
|
import { isTag, isText } from "domhandler";
|
|
4
4
|
// src/style-parser.ts
|
|
5
5
|
/**
|
|
@@ -155,10 +155,11 @@ function parseLegacyColor(value) {
|
|
|
155
155
|
return /^(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(bare) ? parseColor(`#${bare}`) : "";
|
|
156
156
|
}
|
|
157
157
|
/**
|
|
158
|
-
* Parses a CSS `padding` shorthand (1-4 values) into a SpacingValue.
|
|
158
|
+
* Parses a CSS `padding` shorthand (1-4 values) into a SpacingValue. One with
|
|
159
|
+
* a negative side is no padding, since CSS drops the declaration.
|
|
159
160
|
*/
|
|
160
161
|
function parsePaddingShorthand(value) {
|
|
161
|
-
if (!value) return {
|
|
162
|
+
if (!value || hasNegativeLength(value)) return {
|
|
162
163
|
top: 0,
|
|
163
164
|
right: 0,
|
|
164
165
|
bottom: 0,
|
|
@@ -193,16 +194,37 @@ function parsePaddingShorthand(value) {
|
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
/**
|
|
197
|
+
* A padding longhand's px length as written, `0` included, or `undefined` when
|
|
198
|
+
* it doesn't parse or is negative, which makes CSS drop the declaration.
|
|
199
|
+
* Trimmed first so the pattern has one way to match.
|
|
200
|
+
*/
|
|
201
|
+
function explicitPx(value) {
|
|
202
|
+
const trimmed = (value ?? "").trim();
|
|
203
|
+
if (ZERO_LENGTH.test(trimmed)) return 0;
|
|
204
|
+
const match = /^(-?\d+(?:\.\d+)?)(?:px)?$/i.exec(trimmed);
|
|
205
|
+
if (!match) return void 0;
|
|
206
|
+
const px = parseFloat(match[1]);
|
|
207
|
+
return px < 0 ? void 0 : Math.round(px);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Whether a padding value holds a negative length, judged before rounding:
|
|
211
|
+
* `-0.4px` rounds to `-0`, which would pass as valid. A literal `-0` is 0.
|
|
212
|
+
*/
|
|
213
|
+
function hasNegativeLength(value) {
|
|
214
|
+
return value.trim().split(/\s+/).some((part) => parseFloat(part) < 0);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
196
217
|
* Reads CSS padding from a style record, preferring the longhand props
|
|
197
|
-
* (padding-top/right/bottom/left) and falling back to the
|
|
218
|
+
* (padding-top/right/bottom/left), `0` included, and falling back to the
|
|
219
|
+
* `padding` shorthand for a side whose longhand is absent or doesn't parse.
|
|
198
220
|
*/
|
|
199
221
|
function readPaddingFromStyles(styles) {
|
|
200
222
|
const shorthand = parsePaddingShorthand(styles.padding);
|
|
201
223
|
return {
|
|
202
|
-
top:
|
|
203
|
-
right:
|
|
204
|
-
bottom:
|
|
205
|
-
left:
|
|
224
|
+
top: explicitPx(styles["padding-top"]) ?? shorthand.top,
|
|
225
|
+
right: explicitPx(styles["padding-right"]) ?? shorthand.right,
|
|
226
|
+
bottom: explicitPx(styles["padding-bottom"]) ?? shorthand.bottom,
|
|
227
|
+
left: explicitPx(styles["padding-left"]) ?? shorthand.left
|
|
206
228
|
};
|
|
207
229
|
}
|
|
208
230
|
/**
|
|
@@ -224,46 +246,133 @@ function parseAlignment(value, fallback = "left") {
|
|
|
224
246
|
return fallback;
|
|
225
247
|
}
|
|
226
248
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
};
|
|
236
|
-
if (
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
249
|
+
* Whether a CSS colour paints nothing: `transparent`, or an alpha of 0 in a
|
|
250
|
+
* hex (`#rgba`, `#rrggbbaa`) or functional (`rgba()`, `hsl(… / 0%)`) colour.
|
|
251
|
+
* `parseColor` drops the alpha, so ask this before it.
|
|
252
|
+
*/
|
|
253
|
+
function isTransparentColor(value) {
|
|
254
|
+
if (!value) return false;
|
|
255
|
+
const color = value.trim().toLowerCase();
|
|
256
|
+
if (color === "transparent") return true;
|
|
257
|
+
const hex = color.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/);
|
|
258
|
+
if (hex) return /^0+$/.test(hex[1] ?? hex[2]);
|
|
259
|
+
const fn = color.match(/^(?:rgb|hsl)a?\((.*)\)$/);
|
|
260
|
+
if (!fn) return false;
|
|
261
|
+
const alpha = fn[1].includes("/") ? fn[1].split("/")[1] : fn[1].split(",")[3];
|
|
262
|
+
if (alpha === void 0) return false;
|
|
263
|
+
const opacity = parseFloat(alpha);
|
|
264
|
+
return Number.isFinite(opacity) && opacity <= 0;
|
|
265
|
+
}
|
|
266
|
+
const BORDER_STYLE_KEYWORDS = /* @__PURE__ */ new Set([
|
|
267
|
+
"none",
|
|
268
|
+
"hidden",
|
|
269
|
+
"dotted",
|
|
270
|
+
"dashed",
|
|
271
|
+
"solid",
|
|
272
|
+
"double",
|
|
273
|
+
"groove",
|
|
274
|
+
"ridge",
|
|
275
|
+
"inset",
|
|
276
|
+
"outset"
|
|
277
|
+
]);
|
|
278
|
+
const BORDER_WIDTH_TOKEN = /^-?\d+(?:\.\d+)?(?:px)?$/i;
|
|
279
|
+
/**
|
|
280
|
+
* A zero length: `0`, or zero in a CSS length unit, with an optional sign. Run
|
|
281
|
+
* on a trimmed value; each run of zeros has one way to match, so a long run
|
|
282
|
+
* fails in linear time.
|
|
283
|
+
*/
|
|
284
|
+
const ZERO_LENGTH = new RegExp(`^[+-]?(?:0+(?:\\.0+)?|\\.0+)(?:${[
|
|
285
|
+
"px",
|
|
286
|
+
"cm",
|
|
287
|
+
"mm",
|
|
288
|
+
"q",
|
|
289
|
+
"in",
|
|
290
|
+
"pt",
|
|
291
|
+
"pc",
|
|
292
|
+
"em",
|
|
293
|
+
"rem",
|
|
294
|
+
"ex",
|
|
295
|
+
"rex",
|
|
296
|
+
"cap",
|
|
297
|
+
"rcap",
|
|
298
|
+
"ch",
|
|
299
|
+
"rch",
|
|
300
|
+
"ic",
|
|
301
|
+
"ric",
|
|
302
|
+
"lh",
|
|
303
|
+
"rlh",
|
|
304
|
+
"vw",
|
|
305
|
+
"vh",
|
|
306
|
+
"vi",
|
|
307
|
+
"vb",
|
|
308
|
+
"vmin",
|
|
309
|
+
"vmax",
|
|
310
|
+
"svw",
|
|
311
|
+
"svh",
|
|
312
|
+
"svi",
|
|
313
|
+
"svb",
|
|
314
|
+
"svmin",
|
|
315
|
+
"svmax",
|
|
316
|
+
"lvw",
|
|
317
|
+
"lvh",
|
|
318
|
+
"lvi",
|
|
319
|
+
"lvb",
|
|
320
|
+
"lvmin",
|
|
321
|
+
"lvmax",
|
|
322
|
+
"dvw",
|
|
323
|
+
"dvh",
|
|
324
|
+
"dvi",
|
|
325
|
+
"dvb",
|
|
326
|
+
"dvmin",
|
|
327
|
+
"dvmax",
|
|
328
|
+
"cqw",
|
|
329
|
+
"cqh",
|
|
330
|
+
"cqi",
|
|
331
|
+
"cqb",
|
|
332
|
+
"cqmin",
|
|
333
|
+
"cqmax"
|
|
334
|
+
].join("|")})?$`, "i");
|
|
335
|
+
/**
|
|
336
|
+
* One side of a border: a `border` / `border-top` shorthand (`"1px solid
|
|
337
|
+
* #ccc"`, its parts in any order) with that side's longhands applied over it.
|
|
338
|
+
* `width` is the room the border takes, so a `none` or `hidden` border has
|
|
339
|
+
* none. `drawn` is false when the border paints nothing: a stated 0 width, a
|
|
340
|
+
* `none` or `hidden` style, or a transparent colour. Nothing stated reads as
|
|
341
|
+
* drawn.
|
|
342
|
+
*/
|
|
343
|
+
function parseBorderSide(shorthand, longhands = {}) {
|
|
344
|
+
let widthToken;
|
|
345
|
+
let styleToken;
|
|
346
|
+
let colorToken;
|
|
347
|
+
for (const token of cssTokens((shorthand ?? "").trim())) {
|
|
253
348
|
const lower = token.toLowerCase();
|
|
254
|
-
if (
|
|
255
|
-
else if (
|
|
256
|
-
else
|
|
257
|
-
const c = parseColor(lower);
|
|
258
|
-
if (c) color = c;
|
|
259
|
-
}
|
|
349
|
+
if (BORDER_STYLE_KEYWORDS.has(lower)) styleToken = lower;
|
|
350
|
+
else if (BORDER_WIDTH_TOKEN.test(lower) || ZERO_LENGTH.test(lower)) widthToken = lower;
|
|
351
|
+
else if (isTransparentColor(lower) || parseColor(lower)) colorToken = lower;
|
|
260
352
|
}
|
|
353
|
+
widthToken = longhands.width?.trim().toLowerCase() ?? widthToken;
|
|
354
|
+
styleToken = longhands.style?.trim().toLowerCase() ?? styleToken;
|
|
355
|
+
colorToken = longhands.color?.trim().toLowerCase() ?? colorToken;
|
|
356
|
+
const style = styleToken ?? "solid";
|
|
357
|
+
const hidden = style === "none" || style === "hidden";
|
|
358
|
+
const transparent = isTransparentColor(colorToken);
|
|
359
|
+
const zeroWidth = widthToken !== void 0 && ZERO_LENGTH.test(widthToken);
|
|
261
360
|
return {
|
|
262
|
-
width,
|
|
361
|
+
width: hidden ? 0 : parsePxValue(widthToken),
|
|
263
362
|
style,
|
|
264
|
-
color
|
|
363
|
+
color: !transparent && parseColor(colorToken) || "#000000",
|
|
364
|
+
drawn: !zeroWidth && !hidden && !transparent
|
|
265
365
|
};
|
|
266
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* The top or bottom value of a 1–4 value box property, such as
|
|
369
|
+
* `border-width: 0 0 1px` (bottom `1px`). `undefined` when it isn't set.
|
|
370
|
+
*/
|
|
371
|
+
function boxSideValue(value, side) {
|
|
372
|
+
const parts = cssTokens((value ?? "").trim());
|
|
373
|
+
if (parts.length === 0) return void 0;
|
|
374
|
+
return side === "top" ? parts[0] : parts[2] ?? parts[0];
|
|
375
|
+
}
|
|
267
376
|
const X_GRID = [
|
|
268
377
|
"left",
|
|
269
378
|
"center",
|
|
@@ -1532,15 +1641,102 @@ function dividerAlign($el) {
|
|
|
1532
1641
|
return isAuto(left) ? "right" : "left";
|
|
1533
1642
|
}
|
|
1534
1643
|
/**
|
|
1644
|
+
* One side of an `<hr>`'s border: `border-<side>`, else the `border`
|
|
1645
|
+
* shorthand, with `border-<side>-width/-style/-color` and the
|
|
1646
|
+
* `border-width/-style/-color` box values applied over it. `declared` is false
|
|
1647
|
+
* when nothing sets that side.
|
|
1648
|
+
*/
|
|
1649
|
+
function hrSide(styles, side) {
|
|
1650
|
+
const shorthand = styles[`border-${side}`] ?? styles.border;
|
|
1651
|
+
const longhands = {
|
|
1652
|
+
width: styles[`border-${side}-width`] ?? boxSideValue(styles["border-width"], side),
|
|
1653
|
+
style: styles[`border-${side}-style`] ?? boxSideValue(styles["border-style"], side),
|
|
1654
|
+
color: styles[`border-${side}-color`] ?? boxSideValue(styles["border-color"], side)
|
|
1655
|
+
};
|
|
1656
|
+
const declared = shorthand !== void 0 || Object.values(longhands).some((value) => value !== void 0);
|
|
1657
|
+
return {
|
|
1658
|
+
...parseBorderSide(shorthand, longhands),
|
|
1659
|
+
declared
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* The border that draws an `<hr>`'s line: the top border when it draws, else
|
|
1664
|
+
* a drawn bottom border, the common `border: 0; border-bottom: 1px solid #ccc`
|
|
1665
|
+
* email `<hr>`. With neither declared, the top reads as the browser's default
|
|
1666
|
+
* line.
|
|
1667
|
+
*/
|
|
1668
|
+
function hrBorder(styles) {
|
|
1669
|
+
const top = hrSide(styles, "top");
|
|
1670
|
+
if (top.declared && top.drawn) return top;
|
|
1671
|
+
const bottom = hrSide(styles, "bottom");
|
|
1672
|
+
return bottom.declared && bottom.drawn ? bottom : top;
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* The line an `<hr>`'s background draws, the usual email `<hr>`
|
|
1676
|
+
* (`border: 0; height: 1px; background: #ccc`): its colour and `height`.
|
|
1677
|
+
* `undefined` when the background is unset or transparent, or the height is 0.
|
|
1678
|
+
*/
|
|
1679
|
+
function hrBackgroundLine(styles) {
|
|
1680
|
+
const raw = styles["background-color"] ?? backgroundShorthandColor(styles.background);
|
|
1681
|
+
if (!raw || isTransparentColor(raw)) return void 0;
|
|
1682
|
+
const color = parseColor(raw);
|
|
1683
|
+
const thickness = parsePxValue(styles.height);
|
|
1684
|
+
return color && thickness > 0 ? {
|
|
1685
|
+
color,
|
|
1686
|
+
thickness
|
|
1687
|
+
} : void 0;
|
|
1688
|
+
}
|
|
1689
|
+
/** An element's top and bottom margins in px, from the shorthand and longhands. */
|
|
1690
|
+
function verticalMargins(styles) {
|
|
1691
|
+
const parts = (styles.margin ?? "").trim().split(/\s+/).filter(Boolean);
|
|
1692
|
+
const top = parts[0] ?? "";
|
|
1693
|
+
const bottom = parts.length >= 3 ? parts[2] : top;
|
|
1694
|
+
return {
|
|
1695
|
+
top: parsePxValue(styles["margin-top"] ?? top),
|
|
1696
|
+
bottom: parsePxValue(styles["margin-bottom"] ?? bottom)
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* The room an `<hr>` that draws nothing takes: its vertical margins and
|
|
1701
|
+
* padding, its `height` and the widths of transparent top and bottom borders,
|
|
1702
|
+
* never below 0 (negative margins pull content up; a spacer can't). CSS drops
|
|
1703
|
+
* a negative padding or height, so neither counts. `undefined` when a border
|
|
1704
|
+
* or its background draws a line. An `<hr>` with no border styles draws the
|
|
1705
|
+
* browser's default line.
|
|
1706
|
+
*/
|
|
1707
|
+
function undrawnHrHeight($el) {
|
|
1708
|
+
const styles = getStyles$1($el);
|
|
1709
|
+
if (hrBorder(styles).drawn || hrBackgroundLine(styles)) return void 0;
|
|
1710
|
+
const margin = verticalMargins(styles);
|
|
1711
|
+
const padding = readPaddingFromStyles(styles);
|
|
1712
|
+
return Math.max(0, margin.top + padding.top + hrSide(styles, "top").width + Math.max(0, parsePxValue(styles.height)) + hrSide(styles, "bottom").width + padding.bottom + margin.bottom);
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* The report entry for an element that imports no block, or `undefined` when
|
|
1716
|
+
* it converts: an `<hr>` that draws nothing and takes no space.
|
|
1717
|
+
*/
|
|
1718
|
+
function skippedElementEntry($el, $) {
|
|
1719
|
+
const { $el: $target } = resolveWrappedBlock($el, $);
|
|
1720
|
+
if (tagOf($target[0]) !== "hr" || undrawnHrHeight($target) !== 0) return;
|
|
1721
|
+
return {
|
|
1722
|
+
sourceTag: "hr",
|
|
1723
|
+
templaticalBlockType: null,
|
|
1724
|
+
status: "skipped",
|
|
1725
|
+
note: "The divider draws no line and takes no space."
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1535
1729
|
* <hr> → Divider block, with a note for each thing the block does not keep.
|
|
1730
|
+
* The line is the top border, or the background when the border draws nothing.
|
|
1536
1731
|
*
|
|
1537
1732
|
* Templatical centres every divider, so a partial one the source places at
|
|
1538
1733
|
* either side is reported. A full-width line has no placement to lose.
|
|
1539
1734
|
*/
|
|
1540
1735
|
function convertDivider($el, room) {
|
|
1541
1736
|
const styles = getStyles$1($el);
|
|
1542
|
-
const border =
|
|
1543
|
-
const
|
|
1737
|
+
const border = hrBorder(styles);
|
|
1738
|
+
const background = border.drawn ? void 0 : hrBackgroundLine(styles);
|
|
1739
|
+
const lineStyle = !background && (border.style === "dashed" || border.style === "dotted") ? border.style : "solid";
|
|
1544
1740
|
const notes = [];
|
|
1545
1741
|
const width = readDividerWidth($el, room, notes);
|
|
1546
1742
|
const align = dividerAlign($el);
|
|
@@ -1548,8 +1744,8 @@ function convertDivider($el, room) {
|
|
|
1548
1744
|
return {
|
|
1549
1745
|
block: createDividerBlock({
|
|
1550
1746
|
lineStyle,
|
|
1551
|
-
color: border.color || "#e5e7eb",
|
|
1552
|
-
thickness: border.width || 1,
|
|
1747
|
+
color: background?.color ?? (border.color || "#e5e7eb"),
|
|
1748
|
+
thickness: background?.thickness ?? (border.width || 1),
|
|
1553
1749
|
width,
|
|
1554
1750
|
styles: { padding: readPaddingFromStyles(styles) }
|
|
1555
1751
|
}),
|
|
@@ -1704,6 +1900,24 @@ function convertElement($el, $, room) {
|
|
|
1704
1900
|
};
|
|
1705
1901
|
}
|
|
1706
1902
|
if (tag === "hr") {
|
|
1903
|
+
const height = undrawnHrHeight($target);
|
|
1904
|
+
if (height !== void 0) return {
|
|
1905
|
+
block: createSpacerBlock({
|
|
1906
|
+
height,
|
|
1907
|
+
styles: { padding: {
|
|
1908
|
+
top: 0,
|
|
1909
|
+
right: 0,
|
|
1910
|
+
bottom: 0,
|
|
1911
|
+
left: 0
|
|
1912
|
+
} }
|
|
1913
|
+
}),
|
|
1914
|
+
entry: {
|
|
1915
|
+
sourceTag: tag,
|
|
1916
|
+
templaticalBlockType: "spacer",
|
|
1917
|
+
status: "converted",
|
|
1918
|
+
note: `The divider draws no line; imported as a ${height}px spacer.`
|
|
1919
|
+
}
|
|
1920
|
+
};
|
|
1707
1921
|
const { block, notes } = convertDivider($target, room);
|
|
1708
1922
|
return {
|
|
1709
1923
|
block,
|
|
@@ -2522,6 +2736,11 @@ function extractContentBlocks($host, $, entries, warnings, room) {
|
|
|
2522
2736
|
blocks.push(...extractContentBlocks($child, $, entries, warnings, inner));
|
|
2523
2737
|
return;
|
|
2524
2738
|
}
|
|
2739
|
+
const skipped = skippedElementEntry($child, $);
|
|
2740
|
+
if (skipped) {
|
|
2741
|
+
entries.push(skipped);
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2525
2744
|
const r = convertElement($child, $, inner);
|
|
2526
2745
|
if (r) {
|
|
2527
2746
|
entries.push(r.entry);
|
|
@@ -2652,6 +2871,26 @@ function processTable($table, $, entries, warnings, flattenInline = false, enclo
|
|
|
2652
2871
|
return sections;
|
|
2653
2872
|
}
|
|
2654
2873
|
// src/converter.ts
|
|
2874
|
+
/**
|
|
2875
|
+
* The default language: the source's tag, else `defaultLocale`, else
|
|
2876
|
+
* `"en"`. A value that isn't a language tag after `_` → `-` is skipped,
|
|
2877
|
+
* with a warning naming it.
|
|
2878
|
+
*/
|
|
2879
|
+
function defaultLanguage(declared, defaultLocale, warnings) {
|
|
2880
|
+
const rejected = [];
|
|
2881
|
+
let locale = "en";
|
|
2882
|
+
for (const [value, name] of [[declared, "the source's default language"], [defaultLocale, "defaultLocale"]]) {
|
|
2883
|
+
if (!value) continue;
|
|
2884
|
+
const tag = value.replaceAll("_", "-");
|
|
2885
|
+
if (isLocaleTag(tag)) {
|
|
2886
|
+
locale = tag;
|
|
2887
|
+
break;
|
|
2888
|
+
}
|
|
2889
|
+
rejected.push(`Ignored ${name} "${value}": unsupported language tag.`);
|
|
2890
|
+
}
|
|
2891
|
+
for (const message of rejected) warnings.push(`${message} Used "${locale}".`);
|
|
2892
|
+
return locale;
|
|
2893
|
+
}
|
|
2655
2894
|
function emptyPadding() {
|
|
2656
2895
|
return {
|
|
2657
2896
|
top: 0,
|
|
@@ -2769,6 +3008,11 @@ function processBody($, entries, warnings, bodyWidth) {
|
|
|
2769
3008
|
walkContainer($inner);
|
|
2770
3009
|
return;
|
|
2771
3010
|
}
|
|
3011
|
+
const skipped = skippedElementEntry($inner, $);
|
|
3012
|
+
if (skipped) {
|
|
3013
|
+
entries.push(skipped);
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
2772
3016
|
const r = convertElement($inner, $, bodyWidth);
|
|
2773
3017
|
if (r) {
|
|
2774
3018
|
entries.push(r.entry);
|
|
@@ -2789,6 +3033,11 @@ function processBody($, entries, warnings, bodyWidth) {
|
|
|
2789
3033
|
flushLoose();
|
|
2790
3034
|
return;
|
|
2791
3035
|
}
|
|
3036
|
+
const skipped = skippedElementEntry($child, $);
|
|
3037
|
+
if (skipped) {
|
|
3038
|
+
entries.push(skipped);
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
2792
3041
|
const r = convertElement($child, $, bodyWidth);
|
|
2793
3042
|
if (r) {
|
|
2794
3043
|
entries.push(r.entry);
|
|
@@ -2831,7 +3080,7 @@ function pageImageWarning($) {
|
|
|
2831
3080
|
* console.log(report.warnings);
|
|
2832
3081
|
* ```
|
|
2833
3082
|
*/
|
|
2834
|
-
function convertHtmlTemplate(html) {
|
|
3083
|
+
function convertHtmlTemplate(html, options = {}) {
|
|
2835
3084
|
if (typeof html !== "string") throw new Error("Invalid HTML template: expected a string. Pass the raw HTML source as a string.");
|
|
2836
3085
|
if (html.trim().length === 0) throw new Error("Invalid HTML template: input is empty. Pass the raw HTML source of an email.");
|
|
2837
3086
|
const $ = load(html);
|
|
@@ -2841,6 +3090,7 @@ function convertHtmlTemplate(html) {
|
|
|
2841
3090
|
const entries = [];
|
|
2842
3091
|
const warnings = [];
|
|
2843
3092
|
const settings = extractSettings($, linkUnderline);
|
|
3093
|
+
settings.locale = defaultLanguage($("html").attr("lang")?.trim(), options.defaultLocale, warnings);
|
|
2844
3094
|
const pageImage = pageImageWarning($);
|
|
2845
3095
|
if (pageImage) warnings.push(pageImage);
|
|
2846
3096
|
const blocks = processBody($, entries, warnings, settings.width);
|