@emailens/engine 0.1.0 → 0.3.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/README.md CHANGED
@@ -218,9 +218,107 @@ Look up a client by ID.
218
218
  | HEY Mail | `hey-mail` | Webmail | WebKit | Yes |
219
219
  | Superhuman | `superhuman` | Desktop | Blink | Yes |
220
220
 
221
+ ## AI-Powered Fixes (v0.2.0)
222
+
223
+ The engine classifies every warning as either `css` (CSS-only swap) or `structural` (requires HTML restructuring — tables, VML, conditionals). For structural issues that static snippets can't solve, the engine can generate a structured prompt and delegate to an LLM.
224
+
225
+ The engine is **provider-agnostic** — you bring your own AI provider via a simple callback.
226
+
227
+ ### `generateAiFix(options): Promise<AiFixResult>`
228
+
229
+ Builds a fix prompt from the engine's analysis, sends it to your AI provider, and extracts the fixed code.
230
+
231
+ ```typescript
232
+ import Anthropic from "@anthropic-ai/sdk";
233
+ import {
234
+ analyzeEmail,
235
+ generateCompatibilityScore,
236
+ generateAiFix,
237
+ AI_FIX_SYSTEM_PROMPT,
238
+ } from "@emailens/engine";
239
+
240
+ const anthropic = new Anthropic();
241
+ const warnings = analyzeEmail(html, "jsx");
242
+ const scores = generateCompatibilityScore(warnings);
243
+
244
+ const result = await generateAiFix({
245
+ originalHtml: html,
246
+ warnings,
247
+ scores,
248
+ scope: "all", // or "current" with selectedClientId
249
+ format: "jsx",
250
+ provider: async (prompt) => {
251
+ const msg = await anthropic.messages.create({
252
+ model: "claude-sonnet-4-6",
253
+ max_tokens: 8192,
254
+ system: AI_FIX_SYSTEM_PROMPT,
255
+ messages: [{ role: "user", content: prompt }],
256
+ });
257
+ return msg.content[0].type === "text" ? msg.content[0].text : "";
258
+ },
259
+ });
260
+
261
+ console.log(result.code); // Fixed email code
262
+ console.log(result.targetedWarnings); // 23
263
+ console.log(result.structuralCount); // 5
264
+ ```
265
+
266
+ ### `estimateAiFixTokens(options): Promise<TokenEstimate>`
267
+
268
+ Estimate tokens **before** making an API call. Use for cost estimates, limit checks, and UI feedback.
269
+
270
+ ```typescript
271
+ import { estimateAiFixTokens } from "@emailens/engine";
272
+
273
+ const estimate = await estimateAiFixTokens({
274
+ originalHtml: html,
275
+ warnings,
276
+ scores,
277
+ scope: "all",
278
+ format: "jsx",
279
+ maxInputTokens: 16000, // optional, triggers smart truncation
280
+ });
281
+
282
+ console.log(`~${estimate.inputTokens} input tokens`);
283
+ console.log(`~${estimate.estimatedOutputTokens} output tokens`);
284
+ console.log(`${estimate.warningCount} warnings (${estimate.structuralCount} structural)`);
285
+ console.log(`Truncated: ${estimate.truncated}`);
286
+ ```
287
+
288
+ **Smart truncation** kicks in when the prompt exceeds `maxInputTokens`:
289
+ 1. Deduplicates warnings (same property × severity)
290
+ 2. Removes `info`-level warnings
291
+ 3. Removes CSS-only warnings (keeps structural + errors)
292
+ 4. Trims long fix snippets
293
+
294
+ ### `heuristicTokenCount(text): number`
295
+
296
+ Instant synchronous token estimate (~3.5 chars/token). Within ~10-15% of real Claude tokenizer for HTML/CSS.
297
+
298
+ ```typescript
299
+ import { heuristicTokenCount } from "@emailens/engine";
300
+ const tokens = heuristicTokenCount(html); // instant, no deps
301
+ ```
302
+
303
+ ### `AI_FIX_SYSTEM_PROMPT`
304
+
305
+ Expert system prompt for email compatibility fixes. Pass as the `system` parameter to your LLM call for best results. Includes structural fix patterns (table layouts, VML, MSO conditionals).
306
+
307
+ ### `STRUCTURAL_FIX_PROPERTIES`
308
+
309
+ `Set<string>` of CSS properties that require HTML restructuring (not just CSS swaps). Includes `display:flex`, `display:grid`, `word-break`, `position`, `border-radius` (Outlook), `background-image` (Outlook), and more.
310
+
311
+ ```typescript
312
+ import { STRUCTURAL_FIX_PROPERTIES } from "@emailens/engine";
313
+ STRUCTURAL_FIX_PROPERTIES.has("word-break"); // true
314
+ STRUCTURAL_FIX_PROPERTIES.has("color"); // false
315
+ ```
316
+
221
317
  ## CSS Support Matrix
222
318
 
223
- The engine includes a comprehensive CSS support matrix (`src/rules/css-support.ts`) covering 30+ CSS properties and HTML elements across all 12 clients. Data sourced from [caniemail.com](https://www.caniemail.com/) with inferred values for HEY Mail and Superhuman based on their rendering engines.
319
+ The engine includes a comprehensive CSS support matrix (`src/rules/css-support.ts`) covering 45+ CSS properties and HTML elements across all 12 clients. Data sourced from [caniemail.com](https://www.caniemail.com/) with inferred values for HEY Mail and Superhuman based on their rendering engines.
320
+
321
+ Properties added in v0.2.0: `word-break`, `overflow-wrap`, `white-space`, `text-overflow`, `vertical-align`, `border-spacing`, `min-width`, `min-height`, `max-height`, `text-shadow`, `background-size`, `background-position`.
224
322
 
225
323
  ## Types
226
324
 
@@ -228,6 +326,8 @@ The engine includes a comprehensive CSS support matrix (`src/rules/css-support.t
228
326
  type SupportLevel = "supported" | "partial" | "unsupported" | "unknown";
229
327
  type Framework = "jsx" | "mjml" | "maizzle";
230
328
  type InputFormat = "html" | Framework;
329
+ type FixType = "css" | "structural";
330
+ type AiProvider = (prompt: string) => Promise<string>;
231
331
 
232
332
  interface EmailClient {
233
333
  id: string;
@@ -246,6 +346,7 @@ interface CSSWarning {
246
346
  suggestion?: string;
247
347
  fix?: CodeFix;
248
348
  fixIsGenericFallback?: boolean;
349
+ fixType?: FixType; // "css" or "structural" (v0.2.0)
249
350
  }
250
351
 
251
352
  interface CodeFix {
@@ -255,6 +356,25 @@ interface CodeFix {
255
356
  description: string;
256
357
  }
257
358
 
359
+ interface AiFixResult {
360
+ code: string;
361
+ prompt: string;
362
+ targetedWarnings: number;
363
+ structuralCount: number;
364
+ tokenEstimate: TokenEstimate;
365
+ }
366
+
367
+ interface TokenEstimate {
368
+ inputTokens: number;
369
+ estimatedOutputTokens: number;
370
+ promptCharacters: number;
371
+ htmlCharacters: number;
372
+ warningCount: number;
373
+ structuralCount: number;
374
+ truncated: boolean;
375
+ warningsRemoved: number;
376
+ }
377
+
258
378
  interface TransformResult {
259
379
  clientId: string;
260
380
  html: string;
@@ -278,7 +398,7 @@ interface DiffResult {
278
398
  bun test
279
399
  ```
280
400
 
281
- 95 tests covering analysis, transformation, dark mode simulation, framework-aware fixes, edge cases, and accuracy benchmarks against real-world email templates.
401
+ 166 tests covering analysis, transformation, dark mode simulation, framework-aware fixes, AI fix generation, token estimation, smart truncation, fixType classification, and accuracy benchmarks against real-world email templates.
282
402
 
283
403
  ## License
284
404
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,97 @@
1
+ type ExportScope = "all" | "current";
2
+ interface ExportPromptOptions {
3
+ originalHtml: string;
4
+ warnings: CSSWarning[];
5
+ scores: Record<string, {
6
+ score: number;
7
+ errors: number;
8
+ warnings: number;
9
+ info: number;
10
+ }>;
11
+ scope: ExportScope;
12
+ selectedClientId?: string;
13
+ format?: "html" | "jsx" | "mjml" | "maizzle";
14
+ }
15
+ declare function generateFixPrompt(options: ExportPromptOptions): string;
16
+
17
+ interface TokenEstimate {
18
+ /** Estimated input tokens (prompt + system prompt) */
19
+ inputTokens: number;
20
+ /** Estimated output tokens (fixed code response) */
21
+ estimatedOutputTokens: number;
22
+ /** Raw character count of the prompt */
23
+ promptCharacters: number;
24
+ /** Character count of just the HTML being fixed */
25
+ htmlCharacters: number;
26
+ /** Total warnings included in the prompt */
27
+ warningCount: number;
28
+ /** How many warnings are structural (need HTML changes) */
29
+ structuralCount: number;
30
+ /** Whether warnings were truncated to fit within limits */
31
+ truncated: boolean;
32
+ /** Number of warnings removed during truncation */
33
+ warningsRemoved: number;
34
+ }
35
+ /**
36
+ * Extended return type from `estimateAiFixTokens()` that includes both the
37
+ * token metrics AND the (potentially truncated) warnings list. The `warnings`
38
+ * field is used internally by `generateAiFix()` to build the prompt with the
39
+ * truncated set, but is NOT exposed in `AiFixResult.tokenEstimate` to keep
40
+ * the public API clean.
41
+ */
42
+ interface TokenEstimateWithWarnings extends TokenEstimate {
43
+ /** The warnings after smart truncation (may be shorter than the input list) */
44
+ warnings: CSSWarning[];
45
+ }
46
+ interface EstimateOptions extends Omit<ExportPromptOptions, "warnings"> {
47
+ warnings: CSSWarning[];
48
+ /**
49
+ * Maximum input tokens to target. If the estimated prompt exceeds
50
+ * this, warnings will be truncated (info first, then duplicates).
51
+ * Defaults to 16000 (~56KB of prompt text).
52
+ */
53
+ maxInputTokens?: number;
54
+ /**
55
+ * Optional precise token counter. If provided, it will be called
56
+ * with the final prompt text for an exact count. Consumers can wire
57
+ * this to `anthropic.messages.countTokens()`.
58
+ */
59
+ tokenCounter?: (text: string) => Promise<number> | number;
60
+ /**
61
+ * Token count for the system prompt. Added to the input token estimate
62
+ * since the system prompt counts against the context window. Defaults
63
+ * to 250 (matching the built-in AI_FIX_SYSTEM_PROMPT). Set to 0 if
64
+ * not using a system prompt, or override for custom system prompts.
65
+ */
66
+ systemPromptTokens?: number;
67
+ }
68
+ /**
69
+ * Estimate tokens for an AI fix prompt BEFORE making the API call.
70
+ * Use this to show cost estimates, check limits, and decide whether
71
+ * to proceed.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const estimate = await estimateAiFixTokens({
76
+ * originalHtml: html,
77
+ * warnings,
78
+ * scores,
79
+ * scope: "all",
80
+ * format: "jsx",
81
+ * });
82
+ *
83
+ * console.log(`~${estimate.inputTokens} input tokens`);
84
+ * console.log(`~${estimate.estimatedOutputTokens} output tokens`);
85
+ * console.log(`Truncated: ${estimate.truncated}`);
86
+ * ```
87
+ */
88
+ declare function estimateAiFixTokens(options: EstimateOptions): Promise<TokenEstimateWithWarnings>;
89
+ /**
90
+ * Quick synchronous heuristic token count. No deps, no API calls.
91
+ * Accuracy: within ~10-15% of real Claude tokenizer for code/HTML.
92
+ */
93
+ declare function heuristicTokenCount(text: string): number;
94
+
1
95
  type SupportLevel = "supported" | "partial" | "unsupported" | "unknown";
2
96
  type Framework = "jsx" | "mjml" | "maizzle";
3
97
  type InputFormat = "html" | Framework;
@@ -15,6 +109,7 @@ interface CodeFix {
15
109
  language: "html" | "css" | "jsx" | "mjml" | "maizzle";
16
110
  description: string;
17
111
  }
112
+ type FixType = "css" | "structural";
18
113
  interface CSSWarning {
19
114
  severity: "error" | "warning" | "info";
20
115
  client: string;
@@ -23,9 +118,27 @@ interface CSSWarning {
23
118
  suggestion?: string;
24
119
  fix?: CodeFix;
25
120
  fixIsGenericFallback?: boolean;
121
+ fixType?: FixType;
26
122
  line?: number;
27
123
  selector?: string;
28
124
  }
125
+ /**
126
+ * Callback that sends a prompt to an LLM and returns the text response.
127
+ * Consumers bring their own AI provider (Anthropic SDK, Vercel AI, etc.).
128
+ */
129
+ type AiProvider = (prompt: string) => Promise<string>;
130
+ interface AiFixResult {
131
+ /** The fixed email code returned by the AI */
132
+ code: string;
133
+ /** The raw prompt that was sent to the AI */
134
+ prompt: string;
135
+ /** Number of warnings the fix was targeting */
136
+ targetedWarnings: number;
137
+ /** How many of those had fixType: "structural" */
138
+ structuralCount: number;
139
+ /** Token estimate for the AI call */
140
+ tokenEstimate: TokenEstimate;
141
+ }
29
142
  interface TransformResult {
30
143
  clientId: string;
31
144
  html: string;
@@ -47,6 +160,68 @@ interface DiffResult {
47
160
  introduced: CSSWarning[];
48
161
  unchanged: CSSWarning[];
49
162
  }
163
+ interface SpamIssue {
164
+ rule: string;
165
+ severity: "error" | "warning" | "info";
166
+ message: string;
167
+ detail?: string;
168
+ }
169
+ interface SpamReport {
170
+ score: number;
171
+ level: "low" | "medium" | "high";
172
+ issues: SpamIssue[];
173
+ }
174
+ interface LinkIssue {
175
+ severity: "error" | "warning" | "info";
176
+ rule: string;
177
+ message: string;
178
+ href?: string;
179
+ text?: string;
180
+ }
181
+ interface LinkReport {
182
+ totalLinks: number;
183
+ issues: LinkIssue[];
184
+ breakdown: {
185
+ https: number;
186
+ http: number;
187
+ mailto: number;
188
+ tel: number;
189
+ anchor: number;
190
+ other: number;
191
+ };
192
+ }
193
+ interface AccessibilityIssue {
194
+ severity: "error" | "warning" | "info";
195
+ rule: string;
196
+ message: string;
197
+ element?: string;
198
+ details?: string;
199
+ }
200
+ interface AccessibilityReport {
201
+ score: number;
202
+ issues: AccessibilityIssue[];
203
+ }
204
+ interface ImageIssue {
205
+ rule: string;
206
+ severity: "error" | "warning" | "info";
207
+ message: string;
208
+ src?: string;
209
+ }
210
+ interface ImageInfo {
211
+ src: string;
212
+ alt: string | null;
213
+ width: string | null;
214
+ height: string | null;
215
+ isTrackingPixel: boolean;
216
+ dataUriBytes: number;
217
+ issues: string[];
218
+ }
219
+ interface ImageReport {
220
+ total: number;
221
+ totalDataUriBytes: number;
222
+ issues: ImageIssue[];
223
+ images: ImageInfo[];
224
+ }
50
225
 
51
226
  declare const EMAIL_CLIENTS: EmailClient[];
52
227
  declare function getClient(id: string): EmailClient | undefined;
@@ -137,20 +312,103 @@ declare function diffResults(before: {
137
312
  warnings: CSSWarning[];
138
313
  }): DiffResult[];
139
314
 
140
- type ExportScope = "all" | "current";
141
- interface ExportPromptOptions {
142
- originalHtml: string;
143
- warnings: CSSWarning[];
144
- scores: Record<string, {
145
- score: number;
146
- errors: number;
147
- warnings: number;
148
- info: number;
149
- }>;
150
- scope: ExportScope;
151
- selectedClientId?: string;
152
- format?: "html" | "jsx" | "mjml" | "maizzle";
315
+ interface GenerateAiFixOptions extends ExportPromptOptions {
316
+ /** Callback that sends a prompt to an LLM and returns the response text. */
317
+ provider: AiProvider;
318
+ /**
319
+ * Maximum input tokens for the prompt. If the estimated prompt exceeds
320
+ * this, warnings are intelligently truncated (info first, then CSS-only
321
+ * duplicates). Defaults to 16000.
322
+ */
323
+ maxInputTokens?: number;
153
324
  }
154
- declare function generateFixPrompt(options: ExportPromptOptions): string;
325
+ /**
326
+ * Generate an AI-powered fix for email compatibility issues.
327
+ *
328
+ * This uses the deterministic engine's analysis (warnings, scores, fix snippets)
329
+ * to build a structured prompt, then delegates to an LLM for context-aware
330
+ * structural fixes that static snippets cannot handle.
331
+ *
332
+ * The engine stays provider-agnostic — consumers pass their own `AiProvider`
333
+ * callback (Anthropic SDK, Vercel AI SDK, OpenRouter, etc.).
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * import Anthropic from "@anthropic-ai/sdk";
338
+ * import { analyzeEmail, generateCompatibilityScore, generateAiFix } from "@emailens/engine";
339
+ *
340
+ * const anthropic = new Anthropic();
341
+ * const warnings = analyzeEmail(html, "jsx");
342
+ * const scores = generateCompatibilityScore(warnings);
343
+ *
344
+ * // 1. Check cost before calling
345
+ * const estimate = await estimateAiFixTokens({
346
+ * originalHtml: html, warnings, scores, scope: "all", format: "jsx",
347
+ * });
348
+ * console.log(`~${estimate.inputTokens} input tokens`);
349
+ *
350
+ * // 2. Generate the fix
351
+ * const result = await generateAiFix({
352
+ * originalHtml: html, warnings, scores, scope: "all", format: "jsx",
353
+ * provider: async (prompt) => {
354
+ * const msg = await anthropic.messages.create({
355
+ * model: "claude-sonnet-4-6",
356
+ * max_tokens: 8192,
357
+ * system: AI_FIX_SYSTEM_PROMPT,
358
+ * messages: [{ role: "user", content: prompt }],
359
+ * });
360
+ * return msg.content[0].type === "text" ? msg.content[0].text : "";
361
+ * },
362
+ * });
363
+ * ```
364
+ */
365
+ declare function generateAiFix(options: GenerateAiFixOptions): Promise<AiFixResult>;
366
+ /**
367
+ * System prompt for the AI fix provider. Consumers should pass this as
368
+ * the `system` parameter to their LLM call for best results.
369
+ */
370
+ declare const AI_FIX_SYSTEM_PROMPT = "You are an expert email developer specializing in cross-client HTML email compatibility. You fix emails to render correctly across all email clients.\n\nRules:\n- Return ONLY the fixed code inside a single code fence. No explanations before or after.\n- Preserve all existing content, text, links, and visual design.\n- For structural issues (fixType: \"structural\"), you MUST restructure the HTML \u2014 CSS-only changes will not work.\n- Common structural patterns:\n - word-break/overflow-wrap unsupported \u2192 wrap text in <table><tr><td> with constrained width\n - display:flex/grid \u2192 convert to <table> layout (match the original column count and proportions)\n - border-radius in Outlook \u2192 use VML <v:roundrect> with <!--[if mso]> conditionals\n - background-image in Outlook \u2192 use VML <v:rect> with <v:fill>\n - max-width in Outlook \u2192 wrap in <!--[if mso]><table width=\"N\"> conditional\n - position:absolute \u2192 use <table> cells for layout\n - <svg> \u2192 replace with <img> pointing to a hosted PNG\n- For CSS-only issues (fixType: \"css\"), swap properties or add fallbacks.\n- Apply ALL fixes from the issues list \u2014 do not skip any.\n- Use the framework syntax specified (JSX/MJML/Maizzle/HTML).\n- For JSX: use camelCase style props, React Email components, and proper TypeScript types.\n- For MJML: use mj-* elements and attributes.\n- For Maizzle: use Tailwind CSS classes.";
371
+
372
+ /**
373
+ * Properties that require HTML structural changes (not just CSS swaps)
374
+ * to fix. These cannot be solved by replacing one CSS value with another.
375
+ */
376
+ declare const STRUCTURAL_FIX_PROPERTIES: Set<string>;
377
+
378
+ /**
379
+ * Analyze an HTML email for spam indicators.
380
+ *
381
+ * Returns a 0–100 score (100 = clean, 0 = very spammy) and an array
382
+ * of issues found. Uses heuristic rules modeled after common spam
383
+ * filter triggers (CAN-SPAM, GDPR, SpamAssassin patterns).
384
+ */
385
+ declare function analyzeSpam(html: string): SpamReport;
386
+
387
+ /**
388
+ * Extract and validate all links from an HTML email.
389
+ *
390
+ * Performs static analysis only (no network requests). Checks for
391
+ * empty/placeholder hrefs, javascript: protocol, insecure HTTP,
392
+ * generic link text, accessibility issues, and more.
393
+ */
394
+ declare function validateLinks(html: string): LinkReport;
395
+
396
+ /**
397
+ * Audit an HTML email for accessibility issues.
398
+ *
399
+ * Checks for missing lang attributes, image alt text, small fonts,
400
+ * layout table roles, link accessibility, heading hierarchy, and
401
+ * color contrast. Returns a 0–100 score and detailed issues.
402
+ */
403
+ declare function checkAccessibility(html: string): AccessibilityReport;
404
+
405
+ /**
406
+ * Analyze images in an HTML email for best practices.
407
+ *
408
+ * Checks for missing dimensions, oversized data URIs, missing alt
409
+ * attributes, unsupported formats (WebP, SVG), tracking pixels,
410
+ * missing display:block, and overall image heaviness.
411
+ */
412
+ declare function analyzeImages(html: string): ImageReport;
155
413
 
156
- export { type CSSWarning, type CodeFix, type DiffResult, EMAIL_CLIENTS, type EmailClient, type ExportPromptOptions, type ExportScope, type Framework, type InputFormat, type PreviewResult, type SupportLevel, type TransformResult, analyzeEmail, diffResults, generateCompatibilityScore, generateFixPrompt, getClient, getCodeFix, getSuggestion, simulateDarkMode, transformForAllClients, transformForClient };
414
+ export { AI_FIX_SYSTEM_PROMPT, type AccessibilityIssue, type AccessibilityReport, type AiFixResult, type AiProvider, type CSSWarning, type CodeFix, type DiffResult, EMAIL_CLIENTS, type EmailClient, type EstimateOptions, type ExportPromptOptions, type ExportScope, type FixType, type Framework, type GenerateAiFixOptions, type ImageInfo, type ImageIssue, type ImageReport, type InputFormat, type LinkIssue, type LinkReport, type PreviewResult, STRUCTURAL_FIX_PROPERTIES, type SpamIssue, type SpamReport, type SupportLevel, type TokenEstimate, type TokenEstimateWithWarnings, type TransformResult, analyzeEmail, analyzeImages, analyzeSpam, checkAccessibility, diffResults, estimateAiFixTokens, generateAiFix, generateCompatibilityScore, generateFixPrompt, getClient, getCodeFix, getSuggestion, heuristicTokenCount, simulateDarkMode, transformForAllClients, transformForClient, validateLinks };