@templatical/import-html 0.53.0 → 0.54.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +274 -45
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -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);
|
|
@@ -2789,6 +3008,11 @@ function processBody($, entries, warnings, bodyWidth) {
|
|
|
2789
3008
|
walkContainer($inner);
|
|
2790
3009
|
return;
|
|
2791
3010
|
}
|
|
3011
|
+
const skipped = skippedElementEntry($inner, $);
|
|
3012
|
+
if (skipped) {
|
|
3013
|
+
entries.push(skipped);
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
2792
3016
|
const r = convertElement($inner, $, bodyWidth);
|
|
2793
3017
|
if (r) {
|
|
2794
3018
|
entries.push(r.entry);
|
|
@@ -2809,6 +3033,11 @@ function processBody($, entries, warnings, bodyWidth) {
|
|
|
2809
3033
|
flushLoose();
|
|
2810
3034
|
return;
|
|
2811
3035
|
}
|
|
3036
|
+
const skipped = skippedElementEntry($child, $);
|
|
3037
|
+
if (skipped) {
|
|
3038
|
+
entries.push(skipped);
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
2812
3041
|
const r = convertElement($child, $, bodyWidth);
|
|
2813
3042
|
if (r) {
|
|
2814
3043
|
entries.push(r.entry);
|