@hegemonart/get-design-done 1.33.6 → 1.34.2

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.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * email/validate-email-html.cjs — static email-HTML constraint validator
3
+ * (Phase 34.2-01).
4
+ *
5
+ * Pure, deterministic regex/string analysis of an email-HTML STRING. Same
6
+ * input -> identical output. It checks the statically-verifiable SUBSET of the
7
+ * constraint catalogue in `reference/email-design.md` §8 — the spec is the
8
+ * authority; the `rule` ids emitted here are constraint-ids defined there.
9
+ *
10
+ * WHAT IS CHECKED (the four deterministic classes, emitted in a stable order):
11
+ * EM-LAYOUT-01 no `display:flex` / `display:grid` / `position:absolute|fixed`
12
+ * (and `sticky`) in any style — modern box primitives email
13
+ * clients drop. (Presence is flagged; table layout is expected.)
14
+ * EM-STYLE-01 no `<style>` block used as the PRIMARY styling mechanism. A
15
+ * `<style>` block is flagged when, after removing `@media { … }`
16
+ * groups, `@font-face`, `@import` and comments, residual CSS
17
+ * rules remain OR the block exceeds a generous size threshold.
18
+ * A small `@media`-only dark-mode/responsive block (EM-STYLE-04)
19
+ * is tolerated.
20
+ * EM-MSO-01 a full-email document (has <html>/<body> or a layout <table>)
21
+ * contains at least one MSO conditional comment
22
+ * (`<!--[if mso]>` / `<!--[if !mso]>`). Absence is flagged.
23
+ * A bare fragment is NOT flagged.
24
+ * EM-DARK-01 a color-scheme signal is present — a `<meta name="color-scheme">`
25
+ * and/or a CSS `color-scheme:` declaration and/or a
26
+ * `prefers-color-scheme` query. Total absence is flagged. A meta
27
+ * alone satisfies it (decoupled from any <style>).
28
+ *
29
+ * WHAT IS *NOT* CHECKED (catalogued in reference/email-design.md as render-tested
30
+ * guidance — verified by the optional Litmus / Email-on-Acid connection at
31
+ * 34.2-02, never by this validator): ~600px width, ghost tables/VML, bulletproof
32
+ * buttons, image width/height/alt, per-client (EM-CLIENT-01..20) pixel quirks.
33
+ *
34
+ * PURITY (D-02 / D-10): operates only on the passed string — no fs of the
35
+ * document, no network, no child-process spawn, no mjml runtime import, no Date,
36
+ * no process.env. This file has zero require() calls (node builtins included).
37
+ */
38
+
39
+ 'use strict';
40
+
41
+ // Modern box primitives email clients drop (EM-LAYOUT-01). Whitespace-tolerant.
42
+ const FLEX_RE = /display\s*:\s*flex\b/i;
43
+ const GRID_RE = /display\s*:\s*grid\b/i;
44
+ const POSITION_RE = /position\s*:\s*(?:absolute|fixed|sticky)\b/i;
45
+
46
+ // <style>…</style> block capture (EM-STYLE-01).
47
+ const STYLE_BLOCK_RE = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
48
+ const AT_MEDIA_GROUP_RE = /@media[^{]*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/gi;
49
+ const AT_FONTFACE_GROUP_RE = /@font-face\s*\{[^{}]*\}/gi;
50
+ const AT_IMPORT_RE = /@import[^;]*;/gi;
51
+ const CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
52
+ // A CSS rule = a selector followed by a `{ … }` declaration block.
53
+ const CSS_RULE_RE = /[^{}@;]+\{[^{}]*\}/;
54
+ // Generous size threshold: a <style> body this large is a primary sheet even if
55
+ // it parsed as @media-only (deterministic guard against huge tolerated blocks).
56
+ const STYLE_PRIMARY_CHAR_THRESHOLD = 1024;
57
+
58
+ // MSO conditional comments (EM-MSO-01).
59
+ const MSO_COMMENT_RE = /<!--\[if\s+(?:!\s*)?mso/i;
60
+ // "Full email" signal — only then is a missing MSO comment flagged.
61
+ const FULL_EMAIL_RE = /<html[\s>]|<body[\s>]|<table[\s>]/i;
62
+
63
+ // color-scheme signals (EM-DARK-01) — any one satisfies the check.
64
+ const META_COLOR_SCHEME_RE = /<meta\b[^>]*name\s*=\s*["']?\s*color-scheme\b/i;
65
+ const CSS_COLOR_SCHEME_RE = /color-scheme\s*:/i;
66
+ const PREFERS_COLOR_SCHEME_RE = /prefers-color-scheme/i;
67
+
68
+ /**
69
+ * Decide whether a captured <style> block body is a PRIMARY styling mechanism.
70
+ * Tolerates an @media-only (responsive/dark) block per EM-STYLE-04.
71
+ *
72
+ * @param {string} body raw inner text of a <style>…</style>
73
+ * @returns {boolean} true when the block carries non-@media rules or is oversized
74
+ */
75
+ function isPrimaryStyleBlock(body) {
76
+ if (body.length > STYLE_PRIMARY_CHAR_THRESHOLD) return true;
77
+ const residual = body
78
+ .replace(CSS_COMMENT_RE, '')
79
+ .replace(AT_MEDIA_GROUP_RE, '')
80
+ .replace(AT_FONTFACE_GROUP_RE, '')
81
+ .replace(AT_IMPORT_RE, '');
82
+ return CSS_RULE_RE.test(residual);
83
+ }
84
+
85
+ /**
86
+ * Validate an email-HTML string against the statically-checkable constraint
87
+ * subset of reference/email-design.md §8.
88
+ *
89
+ * @param {string} html the email HTML (the caller reads the file and passes the
90
+ * content — this function never touches the filesystem)
91
+ * @param {{ checks?: string[] }} [opts] reserved for future toggles; default
92
+ * runs all four classes
93
+ * @returns {{ ok: boolean, violations: Array<{ rule: string, detail: string }> }}
94
+ * `ok === (violations.length === 0)`; each violation's `rule` is a catalogued
95
+ * constraint-id and `detail` is a short human string.
96
+ */
97
+ function validateEmailHtml(html, opts) {
98
+ if (typeof html !== 'string') {
99
+ throw new TypeError('validateEmailHtml(html): html must be a string');
100
+ }
101
+ void opts; // reserved
102
+ /** @type {Array<{ rule: string, detail: string }>} */
103
+ const violations = [];
104
+
105
+ // --- EM-LAYOUT-01: no flexbox/grid/absolute|fixed positioning -------------
106
+ const layoutHits = [];
107
+ if (FLEX_RE.test(html)) layoutHits.push('display:flex');
108
+ if (GRID_RE.test(html)) layoutHits.push('display:grid');
109
+ if (POSITION_RE.test(html)) layoutHits.push('position:absolute|fixed|sticky');
110
+ if (layoutHits.length > 0) {
111
+ violations.push({
112
+ rule: 'EM-LAYOUT-01',
113
+ detail: `email layout must use role="presentation" tables, not modern box primitives (found ${layoutHits.join(', ')})`,
114
+ });
115
+ }
116
+
117
+ // --- EM-STYLE-01: no <style> block as the primary styling mechanism -------
118
+ STYLE_BLOCK_RE.lastIndex = 0;
119
+ let m;
120
+ let primaryStyle = false;
121
+ while ((m = STYLE_BLOCK_RE.exec(html)) !== null) {
122
+ if (isPrimaryStyleBlock(m[1])) {
123
+ primaryStyle = true;
124
+ break;
125
+ }
126
+ }
127
+ if (primaryStyle) {
128
+ violations.push({
129
+ rule: 'EM-STYLE-01',
130
+ detail: 'visual styling must be inline; a <style> block with non-@media rules is stripped by Gmail (only a small @media-only block is tolerated)',
131
+ });
132
+ }
133
+
134
+ // --- EM-MSO-01: an MSO conditional comment in a full-email document -------
135
+ if (FULL_EMAIL_RE.test(html) && !MSO_COMMENT_RE.test(html)) {
136
+ violations.push({
137
+ rule: 'EM-MSO-01',
138
+ detail: 'a full email must include an MSO conditional comment (<!--[if mso]> … <![endif]-->) for Outlook\'s Word rendering engine',
139
+ });
140
+ }
141
+
142
+ // --- EM-DARK-01: a color-scheme signal is present -------------------------
143
+ const hasColorScheme =
144
+ META_COLOR_SCHEME_RE.test(html) ||
145
+ CSS_COLOR_SCHEME_RE.test(html) ||
146
+ PREFERS_COLOR_SCHEME_RE.test(html);
147
+ if (!hasColorScheme) {
148
+ violations.push({
149
+ rule: 'EM-DARK-01',
150
+ detail: 'declare a color-scheme signal (<meta name="color-scheme">, CSS color-scheme:, or @media prefers-color-scheme) so clients keep the intended palette',
151
+ });
152
+ }
153
+
154
+ return { ok: violations.length === 0, violations };
155
+ }
156
+
157
+ module.exports = { validateEmailHtml };