@emailens/engine 0.1.0 → 0.2.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 +122 -2
- package/dist/index.d.ts +175 -15
- package/dist/index.js +588 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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;
|
|
@@ -137,20 +250,67 @@ declare function diffResults(before: {
|
|
|
137
250
|
warnings: CSSWarning[];
|
|
138
251
|
}): DiffResult[];
|
|
139
252
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}>;
|
|
150
|
-
scope: ExportScope;
|
|
151
|
-
selectedClientId?: string;
|
|
152
|
-
format?: "html" | "jsx" | "mjml" | "maizzle";
|
|
253
|
+
interface GenerateAiFixOptions extends ExportPromptOptions {
|
|
254
|
+
/** Callback that sends a prompt to an LLM and returns the response text. */
|
|
255
|
+
provider: AiProvider;
|
|
256
|
+
/**
|
|
257
|
+
* Maximum input tokens for the prompt. If the estimated prompt exceeds
|
|
258
|
+
* this, warnings are intelligently truncated (info first, then CSS-only
|
|
259
|
+
* duplicates). Defaults to 16000.
|
|
260
|
+
*/
|
|
261
|
+
maxInputTokens?: number;
|
|
153
262
|
}
|
|
154
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Generate an AI-powered fix for email compatibility issues.
|
|
265
|
+
*
|
|
266
|
+
* This uses the deterministic engine's analysis (warnings, scores, fix snippets)
|
|
267
|
+
* to build a structured prompt, then delegates to an LLM for context-aware
|
|
268
|
+
* structural fixes that static snippets cannot handle.
|
|
269
|
+
*
|
|
270
|
+
* The engine stays provider-agnostic — consumers pass their own `AiProvider`
|
|
271
|
+
* callback (Anthropic SDK, Vercel AI SDK, OpenRouter, etc.).
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```ts
|
|
275
|
+
* import Anthropic from "@anthropic-ai/sdk";
|
|
276
|
+
* import { analyzeEmail, generateCompatibilityScore, generateAiFix } from "@emailens/engine";
|
|
277
|
+
*
|
|
278
|
+
* const anthropic = new Anthropic();
|
|
279
|
+
* const warnings = analyzeEmail(html, "jsx");
|
|
280
|
+
* const scores = generateCompatibilityScore(warnings);
|
|
281
|
+
*
|
|
282
|
+
* // 1. Check cost before calling
|
|
283
|
+
* const estimate = await estimateAiFixTokens({
|
|
284
|
+
* originalHtml: html, warnings, scores, scope: "all", format: "jsx",
|
|
285
|
+
* });
|
|
286
|
+
* console.log(`~${estimate.inputTokens} input tokens`);
|
|
287
|
+
*
|
|
288
|
+
* // 2. Generate the fix
|
|
289
|
+
* const result = await generateAiFix({
|
|
290
|
+
* originalHtml: html, warnings, scores, scope: "all", format: "jsx",
|
|
291
|
+
* provider: async (prompt) => {
|
|
292
|
+
* const msg = await anthropic.messages.create({
|
|
293
|
+
* model: "claude-sonnet-4-6",
|
|
294
|
+
* max_tokens: 8192,
|
|
295
|
+
* system: AI_FIX_SYSTEM_PROMPT,
|
|
296
|
+
* messages: [{ role: "user", content: prompt }],
|
|
297
|
+
* });
|
|
298
|
+
* return msg.content[0].type === "text" ? msg.content[0].text : "";
|
|
299
|
+
* },
|
|
300
|
+
* });
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
declare function generateAiFix(options: GenerateAiFixOptions): Promise<AiFixResult>;
|
|
304
|
+
/**
|
|
305
|
+
* System prompt for the AI fix provider. Consumers should pass this as
|
|
306
|
+
* the `system` parameter to their LLM call for best results.
|
|
307
|
+
*/
|
|
308
|
+
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.";
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Properties that require HTML structural changes (not just CSS swaps)
|
|
312
|
+
* to fix. These cannot be solved by replacing one CSS value with another.
|
|
313
|
+
*/
|
|
314
|
+
declare const STRUCTURAL_FIX_PROPERTIES: Set<string>;
|
|
155
315
|
|
|
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 };
|
|
316
|
+
export { AI_FIX_SYSTEM_PROMPT, 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 InputFormat, type PreviewResult, STRUCTURAL_FIX_PROPERTIES, type SupportLevel, type TokenEstimate, type TokenEstimateWithWarnings, type TransformResult, analyzeEmail, diffResults, estimateAiFixTokens, generateAiFix, generateCompatibilityScore, generateFixPrompt, getClient, getCodeFix, getSuggestion, heuristicTokenCount, simulateDarkMode, transformForAllClients, transformForClient };
|