Skip to content

Commit 663331f

Browse files
Add markdown conversion to gmail recipe (#877)
* adding markdown support for emails via turndown * add schema description to output * strip style tags from markdownified email --------- Co-authored-by: Jesse Andrews <anotherjesse@gmail.com>
1 parent b6681f0 commit 663331f

File tree

4 files changed

+107
-10
lines changed

4 files changed

+107
-10
lines changed

deno.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"merkle-reference": "npm:merkle-reference@^2.0.1",
8484
"multiformats": "npm:multiformats@^13.3.2",
8585
"react": "npm:react@^18.3.1",
86+
"turndown": "npm:turndown@^7.1.2",
8687
"typescript": "npm:typescript",
8788
"vite": "npm:vite@^6.2.1",
8889
"zod-to-json-schema": "npm:zod-to-json-schema@^3.24.1",

deno.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

recipes/gmail.tsx

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,76 @@ import {
1212
UI,
1313
} from "@commontools/builder";
1414
import { Cell } from "@commontools/runner";
15+
import TurndownService from "turndown";
16+
17+
// Initialize turndown service
18+
const turndown = new TurndownService({
19+
headingStyle: "atx",
20+
codeBlockStyle: "fenced",
21+
emDelimiter: "*",
22+
});
23+
24+
turndown.addRule("removeStyleTags", {
25+
filter: ["style"],
26+
replacement: "",
27+
});
1528

1629
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
1730

1831
const EmailProperties = {
19-
id: { type: "string" },
20-
threadId: { type: "string" },
21-
labelIds: { type: "array", items: { type: "string" } },
22-
snippet: { type: "string" },
23-
subject: { type: "string" },
24-
from: { type: "string" },
25-
date: { type: "string" },
26-
to: { type: "string" },
27-
plainText: { type: "string" },
28-
htmlContent: { type: "string" },
32+
id: {
33+
type: "string",
34+
title: "Email ID",
35+
description: "Unique identifier for the email",
36+
},
37+
threadId: {
38+
type: "string",
39+
title: "Thread ID",
40+
description: "Identifier for the email thread",
41+
},
42+
labelIds: {
43+
type: "array",
44+
items: { type: "string" },
45+
title: "Labels",
46+
description: "Gmail labels assigned to the email",
47+
},
48+
snippet: {
49+
type: "string",
50+
title: "Snippet",
51+
description: "Brief preview of the email content",
52+
},
53+
subject: {
54+
type: "string",
55+
title: "Subject",
56+
description: "Email subject line",
57+
},
58+
from: {
59+
type: "string",
60+
title: "From",
61+
description: "Sender's email address",
62+
},
63+
date: {
64+
type: "string",
65+
title: "Date",
66+
description: "Date and time when the email was sent",
67+
},
68+
to: { type: "string", title: "To", description: "Recipient's email address" },
69+
plainText: {
70+
type: "string",
71+
title: "Plain Text Content",
72+
description: "Email content in plain text format (often empty)",
73+
},
74+
htmlContent: {
75+
type: "string",
76+
title: "HTML Content",
77+
description: "Email content in HTML format",
78+
},
79+
markdownContent: {
80+
type: "string",
81+
title: "Markdown Content",
82+
description:
83+
"Email content converted to Markdown format. Often best for processing email contents.",
84+
},
2985
} as const;
3086

3187
const EmailSchema = {
@@ -298,6 +354,22 @@ Accept: application/json
298354
}
299355
}
300356

357+
// Generate markdown content from HTML or plainText
358+
let markdownContent = "";
359+
if (htmlContent) {
360+
try {
361+
// Convert HTML to markdown using our custom converter
362+
markdownContent = turndown.turndown(htmlContent);
363+
} catch (error) {
364+
console.error("Error converting HTML to markdown:", error);
365+
// Fallback to plainText if HTML conversion fails
366+
markdownContent = plainText;
367+
}
368+
} else {
369+
// Use plainText as fallback if no HTML content
370+
markdownContent = plainText;
371+
}
372+
301373
return {
302374
id: messageData.id,
303375
threadId: messageData.threadId,
@@ -309,6 +381,7 @@ Accept: application/json
309381
to: extractEmailAddress(to),
310382
plainText,
311383
htmlContent,
384+
markdownContent,
312385
};
313386
} catch (error) {
314387
console.error("Error processing message part:", error);
@@ -496,6 +569,7 @@ export default recipe(
496569
<th style="padding: 10px;">DATE</th>
497570
<th style="padding: 10px;">SUBJECT</th>
498571
<th style="padding: 10px;">LABEL</th>
572+
<th style="padding: 10px;">CONTENT</th>
499573
</tr>
500574
</thead>
501575
<tbody>
@@ -513,6 +587,14 @@ export default recipe(
513587
(email) => email.labelIds.join(", "),
514588
)}&nbsp;
515589
</td>
590+
<td style="border: 1px solid black; padding: 10px;">
591+
<details>
592+
<summary>Show Markdown</summary>
593+
<pre style="white-space: pre-wrap; max-height: 300px; overflow-y: auto;">
594+
{email.markdownContent}
595+
</pre>
596+
</details>
597+
</td>
516598
</tr>
517599
))}
518600
</tbody>

runner/src/local-build.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as commonBuilder from "@commontools/builder";
44
import * as zod from "zod";
55
import * as zodToJsonSchema from "zod-to-json-schema";
66
import * as merkleReference from "merkle-reference";
7+
import turndown from "turndown";
78

89
let DOMParser: any;
910

@@ -124,6 +125,8 @@ export const tsToExports = async (
124125
return merkleReference;
125126
case "zod-to-json-schema":
126127
return zodToJsonSchema;
128+
case "turndown":
129+
return turndown;
127130
default:
128131
throw new Error(`Module not found: ${moduleName}`);
129132
}

0 commit comments

Comments
 (0)