@emailens/engine 0.9.0 → 0.9.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.
package/README.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@emailens/engine)](https://www.npmjs.com/package/@emailens/engine)
4
4
  [![license](https://img.shields.io/npm/l/@emailens/engine)](./LICENSE)
5
- [![tests](https://img.shields.io/badge/tests-574%20passing-brightgreen)]()
5
+ [![tests](https://img.shields.io/badge/tests-653%20passing-brightgreen)]()
6
6
  [![node](https://img.shields.io/node/v/@emailens/engine)](https://nodejs.org/)
7
+ [![MCP](https://img.shields.io/badge/MCP-Server-blue)](https://github.com/emailens/mcp)
7
8
 
8
9
  **Your email looks perfect in Apple Mail. Gmail strips half the CSS. Outlook renders it in Word.**
9
10
 
@@ -119,6 +120,21 @@ Three entry points:
119
120
  | AI auto-fix | Yes | No | No | No |
120
121
  | Open source | MIT | No | No | Yes (data) |
121
122
 
123
+ ### vs other email libraries
124
+
125
+ `@emailens/engine` sits in the **QA / lint / scoring** slot — it analyzes finished HTML. It's complementary to (not a replacement for) composition and inlining libraries.
126
+
127
+ | | @emailens/engine | [juice](https://github.com/automattic/juice) | [email-comb](https://github.com/codsen/email-comb) | [mjml](https://mjml.io/) | [maizzle](https://maizzle.com/) |
128
+ |---|---|---|---|---|---|
129
+ | Purpose | QA / lint / score | CSS inliner | Unused CSS pruner | MJML → HTML | Tailwind → HTML |
130
+ | Per-client compatibility scoring | Yes | No | No | No | No |
131
+ | Spam / a11y / link / image analysis | Yes | No | No | No | No |
132
+ | AI-powered fix generation | Yes | No | No | No | No |
133
+ | Compose emails | Reads only | Reads only | Reads only | Yes | Yes |
134
+ | CSS inlining | No (pair with juice) | Yes | No | Yes (built-in) | Yes (built-in) |
135
+
136
+ A typical pipeline: write in **mjml** or **maizzle** → inline with **juice** → audit with **@emailens/engine** → ship.
137
+
122
138
  ## Supported Email Clients
123
139
 
124
140
  | Client | ID | Category | Engine | Dark Mode |
@@ -129,6 +145,8 @@ Three entry points:
129
145
  | Outlook 365 | `outlook-web` | Webmail | Outlook Web | Yes |
130
146
  | Outlook (New) | `outlook-windows` | Desktop | Outlook Web | Yes |
131
147
  | Outlook Classic | `outlook-windows-legacy` | Desktop | Microsoft Word | Yes |
148
+ | Outlook iOS | `outlook-ios` | Mobile | Outlook Mobile | Yes |
149
+ | Outlook Android | `outlook-android` | Mobile | Outlook Mobile | Yes |
132
150
  | Apple Mail | `apple-mail-macos` | Desktop | WebKit | Yes |
133
151
  | Apple Mail iOS | `apple-mail-ios` | Mobile | WebKit | Yes |
134
152
  | Yahoo Mail | `yahoo-mail` | Webmail | Yahoo | Yes |
@@ -154,19 +172,20 @@ Covers:
154
172
 
155
173
  ## Roadmap
156
174
 
157
- - [ ] Outlook VML auto-generation
158
- - [ ] GitHub Actions integration (score thresholds in CI)
159
- - [x] Automated caniemail.com data sync
160
- - [ ] Real-time rendering previews
161
- - [ ] MJML/Maizzle source-level linting
162
- - [ ] Plugin system for custom analyzers
175
+ See [ROADMAP.md](./ROADMAP.md) for the full picture (shipped items + items under consideration with rationale).
176
+
177
+ **Shipped:** automated caniemail.com data sync · GitHub Actions integration via `@emailens/cli` and the [Marketplace Action](https://github.com/marketplace/actions/emailens-email-preview-check) · AI-powered fix generation · compile module for JSX/MJML/Maizzle.
178
+
179
+ **Considering:** Outlook VML auto-generation · plugin system for custom analyzers · MJML/Maizzle source-level linting · ESLint plugin · spam corpus tuning · dark-mode accuracy tests.
180
+
181
+ Concrete bugs go in [Issues](https://github.com/emailens/engine/issues). Open-ended ideas live in the roadmap.
163
182
 
164
183
  ## Contributing
165
184
 
166
185
  Contributions are welcome! See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for architecture overview, setup instructions, and PR guidelines.
167
186
 
168
187
  ```bash
169
- bun install && bun test # 580 tests
188
+ bun install && bun test # 653 tests
170
189
  ```
171
190
 
172
191
  ### Data Maintenance
package/dist/index.cjs CHANGED
@@ -10115,45 +10115,104 @@ function auditEmail(html, options) {
10115
10115
  var cheerio13 = __toESM(require("cheerio"), 1);
10116
10116
  function toPlainText(html) {
10117
10117
  const $ = cheerio13.load(html);
10118
- $("style, script").remove();
10118
+ $("style, script, head").remove();
10119
10119
  $("[data-skip-in-text='true']").remove();
10120
- $("hr").replaceWith("\n---\n");
10121
- $("br").replaceWith("\n");
10122
- $("img").each((_, el) => {
10123
- const alt = $(el).attr("alt");
10124
- $(el).replaceWith(alt ? alt : "");
10125
- });
10126
- $("a").each((_, el) => {
10127
- const $el = $(el);
10128
- const href = $el.attr("href") || "";
10129
- const text2 = $el.text().trim();
10130
- if (!href || href === text2) {
10131
- $el.replaceWith(text2 || href);
10132
- } else if (!text2) {
10133
- $el.replaceWith(href);
10134
- } else {
10135
- $el.replaceWith(`${text2} (${href})`);
10120
+ const lines = [];
10121
+ let currentLine = "";
10122
+ const BLOCK_TAGS = /* @__PURE__ */ new Set([
10123
+ "p",
10124
+ "div",
10125
+ "h1",
10126
+ "h2",
10127
+ "h3",
10128
+ "h4",
10129
+ "h5",
10130
+ "h6",
10131
+ "tr",
10132
+ "table",
10133
+ "blockquote",
10134
+ "ul",
10135
+ "ol",
10136
+ "li",
10137
+ "section",
10138
+ "article",
10139
+ "header",
10140
+ "footer",
10141
+ "main",
10142
+ "td",
10143
+ "th"
10144
+ // treat cells as blocks to avoid run-on text
10145
+ ]);
10146
+ const SKIP_TAGS = /* @__PURE__ */ new Set(["style", "script", "head"]);
10147
+ function flushLine() {
10148
+ const trimmed = currentLine.trim();
10149
+ if (trimmed) lines.push(trimmed);
10150
+ currentLine = "";
10151
+ }
10152
+ function walk5(node) {
10153
+ var _a;
10154
+ if (node.type === "text") {
10155
+ const text = node.data.replace(/\s+/g, " ");
10156
+ if (text.trim()) {
10157
+ currentLine += text;
10158
+ }
10159
+ return;
10160
+ }
10161
+ if (node.type !== "tag") return;
10162
+ const el = node;
10163
+ const tag = el.tagName.toLowerCase();
10164
+ if (SKIP_TAGS.has(tag)) return;
10165
+ if (tag === "br") {
10166
+ flushLine();
10167
+ return;
10168
+ }
10169
+ if (tag === "hr") {
10170
+ flushLine();
10171
+ lines.push("---");
10172
+ return;
10173
+ }
10174
+ if (tag === "img") {
10175
+ const alt = (_a = $(el).attr("alt")) == null ? void 0 : _a.trim();
10176
+ if (alt) currentLine += alt;
10177
+ return;
10178
+ }
10179
+ if (tag === "a") {
10180
+ const href = $(el).attr("href") || "";
10181
+ const text = $(el).text().trim();
10182
+ if (!href || href === "#" || href === text) {
10183
+ currentLine += text || href;
10184
+ } else if (!text) {
10185
+ currentLine += href;
10186
+ } else {
10187
+ currentLine += `${text} (${href})`;
10188
+ }
10189
+ return;
10190
+ }
10191
+ const isBlock = BLOCK_TAGS.has(tag);
10192
+ if (isBlock) {
10193
+ flushLine();
10194
+ } else if (currentLine && !currentLine.endsWith(" ")) {
10195
+ currentLine += " ";
10196
+ }
10197
+ if (tag === "li") {
10198
+ currentLine = "- ";
10199
+ }
10200
+ for (const child of el.children) {
10201
+ walk5(child);
10202
+ }
10203
+ if (isBlock) flushLine();
10204
+ }
10205
+ const body = $("body");
10206
+ const root = body.length ? body[0] : $.root()[0];
10207
+ if (root && "children" in root) {
10208
+ for (const child of root.children) {
10209
+ walk5(child);
10136
10210
  }
10137
- });
10138
- $("li").each((_, el) => {
10139
- const $el = $(el);
10140
- const text2 = $el.text().trim();
10141
- $el.replaceWith(`
10142
- - ${text2}
10143
- `);
10144
- });
10145
- const blockTags = "p, div, h1, h2, h3, h4, h5, h6, tr, blockquote, ul, ol";
10146
- $(blockTags).each((_, el) => {
10147
- const $el = $(el);
10148
- $el.prepend("\n\n");
10149
- $el.append("\n\n");
10150
- });
10151
- let text = $("body").text();
10152
- if (!text.trim()) {
10153
- text = $.root().text();
10154
10211
  }
10155
- text = text.replace(/\n{3,}/g, "\n\n");
10156
- return text.trim();
10212
+ flushLine();
10213
+ let result = lines.join("\n");
10214
+ result = result.replace(/\n{3,}/g, "\n\n");
10215
+ return result.trim();
10157
10216
  }
10158
10217
 
10159
10218
  // src/session.ts