@emailens/engine 0.11.0 → 0.11.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/compile/index.d.cts +2 -2
- package/dist/compile/index.d.ts +2 -2
- package/dist/index.cjs +202 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -5
- package/dist/index.d.ts +25 -5
- package/dist/index.js +201 -10
- package/dist/index.js.map +1 -1
- package/dist/{react-email-D24HkW2Z.d.ts → react-email-BROBcC8u.d.ts} +1 -1
- package/dist/{react-email-BWIfwG7U.d.cts → react-email-DuZAgJtw.d.cts} +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/{types-BFhPXh3c.d.cts → types-bvGDId6f.d.cts} +9 -1
- package/dist/{types-BFhPXh3c.d.ts → types-bvGDId6f.d.ts} +9 -1
- package/package.json +1 -1
package/dist/compile/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy, c as compileReactEmail } from '../react-email-
|
|
1
|
+
import { x as InputFormat } from '../types-bvGDId6f.cjs';
|
|
2
|
+
export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy, c as compileReactEmail } from '../react-email-DuZAgJtw.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Compile an MJML source string into an HTML email string.
|
package/dist/compile/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy, c as compileReactEmail } from '../react-email-
|
|
1
|
+
import { x as InputFormat } from '../types-bvGDId6f.js';
|
|
2
|
+
export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy, c as compileReactEmail } from '../react-email-BROBcC8u.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Compile an MJML source string into an HTML email string.
|
package/dist/index.cjs
CHANGED
|
@@ -90,6 +90,7 @@ __export(index_exports, {
|
|
|
90
90
|
checkSize: () => checkSize,
|
|
91
91
|
checkTemplateVariables: () => checkTemplateVariables,
|
|
92
92
|
checkVisual: () => checkVisual,
|
|
93
|
+
checkVml: () => checkVml,
|
|
93
94
|
colorDistance: () => colorDistance,
|
|
94
95
|
contrastRatio: () => contrastRatio,
|
|
95
96
|
createSession: () => createSession,
|
|
@@ -9907,6 +9908,7 @@ var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, pr
|
|
|
9907
9908
|
var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
|
|
9908
9909
|
var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
|
|
9909
9910
|
var EMPTY_OVERFLOW = { hasOverflow: false, issues: [] };
|
|
9911
|
+
var EMPTY_VML = { hasVml: false, issues: [] };
|
|
9910
9912
|
var EMPTY_VISUAL = { issues: [] };
|
|
9911
9913
|
var EMAIL_MAX_WIDTH = 600;
|
|
9912
9914
|
var UNBREAKABLE_STRING_LENGTH = 30;
|
|
@@ -11115,11 +11117,11 @@ function locOfElement(el) {
|
|
|
11115
11117
|
if (!raw) return void 0;
|
|
11116
11118
|
return toLoc((_a = raw.startTag) != null ? _a : raw);
|
|
11117
11119
|
}
|
|
11118
|
-
function locOfAttr(el,
|
|
11120
|
+
function locOfAttr(el, attr2) {
|
|
11119
11121
|
var _a, _b, _c, _d;
|
|
11120
11122
|
const raw = el == null ? void 0 : el.sourceCodeLocation;
|
|
11121
11123
|
if (!raw) return void 0;
|
|
11122
|
-
const attrLoc = (_d = (_a = raw.attrs) == null ? void 0 : _a[
|
|
11124
|
+
const attrLoc = (_d = (_a = raw.attrs) == null ? void 0 : _a[attr2]) != null ? _d : (_c = (_b = raw.startTag) == null ? void 0 : _b.attrs) == null ? void 0 : _c[attr2];
|
|
11123
11125
|
return attrLoc ? toLoc(attrLoc) : locOfElement(el);
|
|
11124
11126
|
}
|
|
11125
11127
|
function locInAttr(attrLoc, source, property, occurrence = 0) {
|
|
@@ -14262,22 +14264,22 @@ function checkTemplateVariablesFromDom($, source) {
|
|
|
14262
14264
|
for (const sel of attrSelectors) {
|
|
14263
14265
|
$(sel).each((_, el) => {
|
|
14264
14266
|
const attrs = ["href", "src", "alt"];
|
|
14265
|
-
for (const
|
|
14266
|
-
const value = $(el).attr(
|
|
14267
|
+
for (const attr2 of attrs) {
|
|
14268
|
+
const value = $(el).attr(attr2);
|
|
14267
14269
|
if (!value) continue;
|
|
14268
14270
|
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
14269
14271
|
pattern.lastIndex = 0;
|
|
14270
14272
|
let match;
|
|
14271
14273
|
while ((match = pattern.exec(value)) !== null) {
|
|
14272
14274
|
const variable = match[0];
|
|
14273
|
-
const key = `attr:${
|
|
14275
|
+
const key = `attr:${attr2}:${variable}`;
|
|
14274
14276
|
if (seen.has(key)) continue;
|
|
14275
14277
|
seen.add(key);
|
|
14276
|
-
const loc = locOfAttr(el,
|
|
14278
|
+
const loc = locOfAttr(el, attr2);
|
|
14277
14279
|
issues.push(__spreadValues({
|
|
14278
14280
|
rule: "unresolved-variable",
|
|
14279
14281
|
severity: "error",
|
|
14280
|
-
message: `Unresolved ${label} variable "${variable}" found in ${
|
|
14282
|
+
message: `Unresolved ${label} variable "${variable}" found in ${attr2} attribute.`,
|
|
14281
14283
|
variable,
|
|
14282
14284
|
location: "attribute"
|
|
14283
14285
|
}, loc ? { loc } : {}));
|
|
@@ -14303,8 +14305,8 @@ function fixedPxWidth($el) {
|
|
|
14303
14305
|
const style = $el.attr("style") || "";
|
|
14304
14306
|
const styleMatch = style.match(/(?:^|[;\s])width\s*:\s*(\d+)px/i);
|
|
14305
14307
|
if (styleMatch) return parseInt(styleMatch[1], 10);
|
|
14306
|
-
const
|
|
14307
|
-
if (
|
|
14308
|
+
const attr2 = $el.attr("width");
|
|
14309
|
+
if (attr2 && /^\d+$/.test(attr2.trim())) return parseInt(attr2.trim(), 10);
|
|
14308
14310
|
return null;
|
|
14309
14311
|
}
|
|
14310
14312
|
function isFluid(style) {
|
|
@@ -14444,6 +14446,193 @@ function checkOverflow(html, options) {
|
|
|
14444
14446
|
);
|
|
14445
14447
|
}
|
|
14446
14448
|
|
|
14449
|
+
// src/vml-checker.ts
|
|
14450
|
+
var SHAPE_TAGS = /* @__PURE__ */ new Set([
|
|
14451
|
+
"rect",
|
|
14452
|
+
"roundrect",
|
|
14453
|
+
"oval",
|
|
14454
|
+
"line",
|
|
14455
|
+
"polyline",
|
|
14456
|
+
"curve",
|
|
14457
|
+
"arc",
|
|
14458
|
+
"shape",
|
|
14459
|
+
"image",
|
|
14460
|
+
"background"
|
|
14461
|
+
]);
|
|
14462
|
+
var GROUP_TAGS = /* @__PURE__ */ new Set(["group"]);
|
|
14463
|
+
function msoBlocks(html) {
|
|
14464
|
+
const blocks = [];
|
|
14465
|
+
const re = /<!--\[if([^\]]*)\]>([\s\S]*?)<!\[endif\]-->/g;
|
|
14466
|
+
let m;
|
|
14467
|
+
while ((m = re.exec(html)) !== null) {
|
|
14468
|
+
const condition = m[1];
|
|
14469
|
+
if (/!\s*(mso|vml)/i.test(condition)) continue;
|
|
14470
|
+
if (!/mso|vml/i.test(condition)) continue;
|
|
14471
|
+
blocks.push({ inner: m[2], offset: m.index + m[0].length - m[2].length - "<![endif]-->".length });
|
|
14472
|
+
}
|
|
14473
|
+
return blocks;
|
|
14474
|
+
}
|
|
14475
|
+
function vmlTags(blocks) {
|
|
14476
|
+
const tags = [];
|
|
14477
|
+
const re = /<(\/?)v:([a-z]+)((?:[^>"']|"[^"]*"|'[^']*')*?)(\/?)>/gi;
|
|
14478
|
+
for (const block of blocks) {
|
|
14479
|
+
let m;
|
|
14480
|
+
re.lastIndex = 0;
|
|
14481
|
+
while ((m = re.exec(block.inner)) !== null) {
|
|
14482
|
+
tags.push({
|
|
14483
|
+
name: m[2].toLowerCase(),
|
|
14484
|
+
closing: m[1] === "/",
|
|
14485
|
+
selfClosing: m[4] === "/",
|
|
14486
|
+
attrs: m[3],
|
|
14487
|
+
offset: block.offset + m.index,
|
|
14488
|
+
length: m[0].length
|
|
14489
|
+
});
|
|
14490
|
+
}
|
|
14491
|
+
}
|
|
14492
|
+
return tags;
|
|
14493
|
+
}
|
|
14494
|
+
function locAt(source, offset, length) {
|
|
14495
|
+
if (source === void 0) return void 0;
|
|
14496
|
+
const start = positionOf(source, offset);
|
|
14497
|
+
const end = positionOf(source, offset + length);
|
|
14498
|
+
return {
|
|
14499
|
+
line: start.line,
|
|
14500
|
+
column: start.column,
|
|
14501
|
+
endLine: end.line,
|
|
14502
|
+
endColumn: end.column,
|
|
14503
|
+
offset,
|
|
14504
|
+
length
|
|
14505
|
+
};
|
|
14506
|
+
}
|
|
14507
|
+
function attr(attrs, name) {
|
|
14508
|
+
var _a;
|
|
14509
|
+
const m = attrs.match(
|
|
14510
|
+
new RegExp(`(?:^|\\s)${name}\\s*=\\s*"([^"]*)"|(?:^|\\s)${name}\\s*=\\s*'([^']*)'`, "i")
|
|
14511
|
+
);
|
|
14512
|
+
return m ? (_a = m[1]) != null ? _a : m[2] : void 0;
|
|
14513
|
+
}
|
|
14514
|
+
function add(issues, seen, key, issue) {
|
|
14515
|
+
const existing = seen.get(key);
|
|
14516
|
+
if (existing) {
|
|
14517
|
+
if (!issue.loc || !existing.locs) return;
|
|
14518
|
+
if (existing.locs.some((l) => l.offset === issue.loc.offset)) return;
|
|
14519
|
+
if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
14520
|
+
existing.locsTruncated = true;
|
|
14521
|
+
return;
|
|
14522
|
+
}
|
|
14523
|
+
existing.locs.push(issue.loc);
|
|
14524
|
+
return;
|
|
14525
|
+
}
|
|
14526
|
+
seen.set(key, issue);
|
|
14527
|
+
issues.push(issue);
|
|
14528
|
+
}
|
|
14529
|
+
function withLoc(issue, loc) {
|
|
14530
|
+
return loc ? __spreadProps(__spreadValues({}, issue), { loc, locs: [loc] }) : issue;
|
|
14531
|
+
}
|
|
14532
|
+
var TEXT_HOSTS = /<\s*(center|v:textbox|div|p|table|h[1-6]|span|font|a)\b/i;
|
|
14533
|
+
function checkLooseText(issues, seen, open, inner, source) {
|
|
14534
|
+
if (TEXT_HOSTS.test(inner)) return;
|
|
14535
|
+
const text = inner.replace(/<!--[\s\S]*?-->/g, "").replace(/<[^>]*>/g, "").replace(/ |\s/g, "");
|
|
14536
|
+
if (text === "") return;
|
|
14537
|
+
add(issues, seen, `loosetext:${open.name}`, withLoc({
|
|
14538
|
+
rule: "vml-unrendered-text",
|
|
14539
|
+
severity: "error",
|
|
14540
|
+
message: `<v:${open.name}> has label text with no element around it, which Outlook does not draw.`,
|
|
14541
|
+
detail: `Verified in Outlook Classic: the shape renders its fill and shape correctly and shows no text at all, so the reader gets a blank coloured block where the label should be. Nothing errors and the HTML fallback still reads correctly in every other client, so this ships unnoticed. Wrap the text in <center> (what the bulletproof-button pattern uses) or in <v:textbox>.`
|
|
14542
|
+
}, locAt(source, open.offset, open.length)));
|
|
14543
|
+
}
|
|
14544
|
+
function checkVml(html, options) {
|
|
14545
|
+
if (!html || !html.trim()) return EMPTY_VML;
|
|
14546
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
14547
|
+
const tags = vmlTags(msoBlocks(html));
|
|
14548
|
+
if (tags.length === 0) return EMPTY_VML;
|
|
14549
|
+
const issues = [];
|
|
14550
|
+
const seen = /* @__PURE__ */ new Map();
|
|
14551
|
+
const openShapes = [];
|
|
14552
|
+
let groupDepth = 0;
|
|
14553
|
+
for (const tag of tags) {
|
|
14554
|
+
const { name } = tag;
|
|
14555
|
+
const isShape = SHAPE_TAGS.has(name);
|
|
14556
|
+
const isGroup = GROUP_TAGS.has(name);
|
|
14557
|
+
const loc = locAt(source, tag.offset, tag.length);
|
|
14558
|
+
if (tag.closing) {
|
|
14559
|
+
if (isGroup) groupDepth = Math.max(0, groupDepth - 1);
|
|
14560
|
+
if (isShape) {
|
|
14561
|
+
const idx = openShapes.map((t) => t.name).lastIndexOf(name);
|
|
14562
|
+
if (idx !== -1) {
|
|
14563
|
+
const open = openShapes[idx];
|
|
14564
|
+
const inner = html.slice(open.offset + open.length, tag.offset);
|
|
14565
|
+
checkLooseText(issues, seen, open, inner, source);
|
|
14566
|
+
}
|
|
14567
|
+
if (idx === -1) {
|
|
14568
|
+
add(issues, seen, `stray:${name}`, withLoc({
|
|
14569
|
+
rule: "vml-unbalanced-tag",
|
|
14570
|
+
severity: "error",
|
|
14571
|
+
message: `Closing </v:${name}> has no matching opening tag.`,
|
|
14572
|
+
detail: `Outlook stops rendering the shape when VML tags do not balance. Check that every conditional-comment block opens and closes the tags it is responsible for.`
|
|
14573
|
+
}, loc));
|
|
14574
|
+
} else {
|
|
14575
|
+
openShapes.splice(idx, 1);
|
|
14576
|
+
}
|
|
14577
|
+
}
|
|
14578
|
+
continue;
|
|
14579
|
+
}
|
|
14580
|
+
if (isGroup && !tag.selfClosing) groupDepth++;
|
|
14581
|
+
if (isShape) {
|
|
14582
|
+
const parent = openShapes[openShapes.length - 1];
|
|
14583
|
+
if (parent && groupDepth === 0) {
|
|
14584
|
+
add(issues, seen, `nested:${parent.name}>${name}`, withLoc({
|
|
14585
|
+
rule: "vml-nested-shape",
|
|
14586
|
+
severity: "error",
|
|
14587
|
+
message: `<v:${name}> is nested inside <v:${parent.name}>. Outlook does not support nesting one VML shape inside another.`,
|
|
14588
|
+
detail: `Verified in Outlook Classic, and the damage is not local. Three things happen: the containing <v:${parent.name}> does not render at all (its fill and everything inside it disappear, leaving the inner shape stranded); the table structure around it terminates early, so content *after* the shape falls out of the layout and renders outside the email frame; and every VML shape further down the email stops drawing its text, so buttons and headings after this point ship as blank coloured blocks. Confirmed with byte-identical probes either side of one nested shape: the one before it renders its labels, the one after it does not. Every other client renders the HTML fallback correctly, which is why none of this is visible outside the Word engine. Fix: lift the inner shape out of <v:${parent.name}>, or drop the container shape and keep the inner one (a framed background can degrade to a solid fill colour on the <td> instead).`
|
|
14589
|
+
}, loc));
|
|
14590
|
+
}
|
|
14591
|
+
if (!tag.selfClosing) openShapes.push(tag);
|
|
14592
|
+
}
|
|
14593
|
+
const style = attr(tag.attrs, "style");
|
|
14594
|
+
if (style && (isShape || isGroup)) {
|
|
14595
|
+
for (const prop of ["width", "height"]) {
|
|
14596
|
+
const m = style.match(new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]*)`, "i"));
|
|
14597
|
+
if (!m) continue;
|
|
14598
|
+
const value = m[1].trim();
|
|
14599
|
+
if (value === "" || /^[a-z%]+$/i.test(value)) {
|
|
14600
|
+
add(issues, seen, `dim:${name}:${prop}:${value}`, withLoc({
|
|
14601
|
+
rule: "vml-invalid-dimension",
|
|
14602
|
+
severity: "error",
|
|
14603
|
+
message: `<v:${name}> has an invalid ${prop}: "${prop}:${value}" is missing its number.`,
|
|
14604
|
+
detail: `Verified in Outlook Classic: the shape still draws, but at a size Outlook picks rather than the one you meant, and the content inside is clipped to it \u2014 roughly half the intended height in the case measured, with the text inside cut off. It fails quietly: there is no gap or broken image to notice, just content that is silently missing. Usually a template variable that resolved to an empty string. Set an explicit value in pixels, e.g. ${prop}:400px.`
|
|
14605
|
+
}, loc));
|
|
14606
|
+
}
|
|
14607
|
+
}
|
|
14608
|
+
}
|
|
14609
|
+
if (name === "roundrect") {
|
|
14610
|
+
const arcsize = attr(tag.attrs, "arcsize");
|
|
14611
|
+
if (arcsize !== void 0) {
|
|
14612
|
+
const raw = arcsize.trim();
|
|
14613
|
+
const pct = raw.endsWith("%") ? parseFloat(raw) : parseFloat(raw) * 100;
|
|
14614
|
+
if (Number.isFinite(pct) && (pct < 0 || pct > 100)) {
|
|
14615
|
+
add(issues, seen, `arcsize:${raw}`, withLoc({
|
|
14616
|
+
rule: "vml-arcsize-range",
|
|
14617
|
+
severity: "warning",
|
|
14618
|
+
message: `<v:roundrect> has arcsize="${raw}", outside the valid 0%\u2013100% range.`,
|
|
14619
|
+
detail: `arcsize is a fraction of half the shape's smaller side: 0% is square, 100% is fully circular. Verified in Outlook Classic: out-of-range values are clamped, so 120% draws exactly the same corner as 100%. Nothing visibly breaks today, which is why this is a warning and not an error \u2014 but the radius you get is the renderer's clamp rather than a value you chose, and it is not guaranteed across clients. For a fully rounded button, say arcsize="100%".`
|
|
14620
|
+
}, loc));
|
|
14621
|
+
}
|
|
14622
|
+
}
|
|
14623
|
+
}
|
|
14624
|
+
}
|
|
14625
|
+
for (const tag of openShapes) {
|
|
14626
|
+
add(issues, seen, `unclosed:${tag.name}`, withLoc({
|
|
14627
|
+
rule: "vml-unbalanced-tag",
|
|
14628
|
+
severity: "error",
|
|
14629
|
+
message: `<v:${tag.name}> is never closed.`,
|
|
14630
|
+
detail: `Outlook needs the matching </v:${tag.name}>, usually in a later conditional-comment block. Without it the shape swallows the rest of the email.`
|
|
14631
|
+
}, locAt(source, tag.offset, tag.length)));
|
|
14632
|
+
}
|
|
14633
|
+
return { hasVml: true, issues };
|
|
14634
|
+
}
|
|
14635
|
+
|
|
14447
14636
|
// src/visual-checker.ts
|
|
14448
14637
|
var csstree8 = __toESM(require("css-tree"), 1);
|
|
14449
14638
|
var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
|
|
@@ -14774,6 +14963,7 @@ var EMPTY_AUDIT = {
|
|
|
14774
14963
|
templateVariables: EMPTY_TEMPLATE,
|
|
14775
14964
|
overflow: EMPTY_OVERFLOW,
|
|
14776
14965
|
visual: EMPTY_VISUAL,
|
|
14966
|
+
vml: EMPTY_VML,
|
|
14777
14967
|
darkContrast: [],
|
|
14778
14968
|
mobileContrast: [],
|
|
14779
14969
|
design: EMPTY_DESIGN
|
|
@@ -14801,13 +14991,14 @@ function runAudit($, html, framework, options) {
|
|
|
14801
14991
|
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
|
|
14802
14992
|
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
|
|
14803
14993
|
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
|
|
14994
|
+
const vml = skip.has("vml") ? EMPTY_VML : checkVml(html, { positions: !!source });
|
|
14804
14995
|
const darkContrast = skip.has("darkContrast") || skip.has("accessibility") ? [] : dedupeByElement([
|
|
14805
14996
|
...checkDarkModeContrast(html, accessibility.issues),
|
|
14806
14997
|
...checkDarkStylesContrastFromDom($, accessibility.issues)
|
|
14807
14998
|
]);
|
|
14808
14999
|
const design = skip.has("design") ? EMPTY_DESIGN : checkDesignConsistencyFromDom($);
|
|
14809
15000
|
const mobileContrast = skip.has("mobileContrast") || skip.has("accessibility") ? [] : checkMobileContrastFromDom($, accessibility.issues);
|
|
14810
|
-
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual, darkContrast, mobileContrast, design };
|
|
15001
|
+
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual, vml, darkContrast, mobileContrast, design };
|
|
14811
15002
|
}
|
|
14812
15003
|
function auditEmail(html, options) {
|
|
14813
15004
|
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
|
|
@@ -15041,6 +15232,7 @@ var CompileError = class extends Error {
|
|
|
15041
15232
|
checkSize,
|
|
15042
15233
|
checkTemplateVariables,
|
|
15043
15234
|
checkVisual,
|
|
15235
|
+
checkVml,
|
|
15044
15236
|
colorDistance,
|
|
15045
15237
|
contrastRatio,
|
|
15046
15238
|
createSession,
|