@cogitator-ai/channels 0.16.0 → 0.17.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.
Files changed (69) hide show
  1. package/README.md +53 -12
  2. package/dist/assistant-config.d.ts +40 -0
  3. package/dist/assistant-config.d.ts.map +1 -1
  4. package/dist/assistant-config.js +66 -0
  5. package/dist/assistant-config.js.map +1 -1
  6. package/dist/channels/email/address.d.ts +44 -0
  7. package/dist/channels/email/address.d.ts.map +1 -0
  8. package/dist/channels/email/address.js +211 -0
  9. package/dist/channels/email/address.js.map +1 -0
  10. package/dist/channels/email/auth.d.ts +59 -0
  11. package/dist/channels/email/auth.d.ts.map +1 -0
  12. package/dist/channels/email/auth.js +212 -0
  13. package/dist/channels/email/auth.js.map +1 -0
  14. package/dist/channels/email/channel.d.ts +256 -0
  15. package/dist/channels/email/channel.d.ts.map +1 -0
  16. package/dist/channels/email/channel.js +701 -0
  17. package/dist/channels/email/channel.js.map +1 -0
  18. package/dist/channels/email/errors.d.ts +11 -0
  19. package/dist/channels/email/errors.d.ts.map +1 -0
  20. package/dist/channels/email/errors.js +13 -0
  21. package/dist/channels/email/errors.js.map +1 -0
  22. package/dist/channels/email/imap.d.ts +125 -0
  23. package/dist/channels/email/imap.d.ts.map +1 -0
  24. package/dist/channels/email/imap.js +309 -0
  25. package/dist/channels/email/imap.js.map +1 -0
  26. package/dist/channels/email/index.d.ts +7 -0
  27. package/dist/channels/email/index.d.ts.map +1 -0
  28. package/dist/channels/email/index.js +3 -0
  29. package/dist/channels/email/index.js.map +1 -0
  30. package/dist/channels/email/mime.d.ts +71 -0
  31. package/dist/channels/email/mime.d.ts.map +1 -0
  32. package/dist/channels/email/mime.js +83 -0
  33. package/dist/channels/email/mime.js.map +1 -0
  34. package/dist/channels/email/multipart.d.ts +28 -0
  35. package/dist/channels/email/multipart.d.ts.map +1 -0
  36. package/dist/channels/email/multipart.js +102 -0
  37. package/dist/channels/email/multipart.js.map +1 -0
  38. package/dist/channels/email/providers.d.ts +49 -0
  39. package/dist/channels/email/providers.d.ts.map +1 -0
  40. package/dist/channels/email/providers.js +292 -0
  41. package/dist/channels/email/providers.js.map +1 -0
  42. package/dist/channels/email/reply.d.ts +9 -0
  43. package/dist/channels/email/reply.d.ts.map +1 -0
  44. package/dist/channels/email/reply.js +122 -0
  45. package/dist/channels/email/reply.js.map +1 -0
  46. package/dist/formatters/html.d.ts +16 -0
  47. package/dist/formatters/html.d.ts.map +1 -0
  48. package/dist/formatters/html.js +535 -0
  49. package/dist/formatters/html.js.map +1 -0
  50. package/dist/formatters/markdown.d.ts.map +1 -1
  51. package/dist/formatters/markdown.js +1 -0
  52. package/dist/formatters/markdown.js.map +1 -1
  53. package/dist/gateway.d.ts +23 -1
  54. package/dist/gateway.d.ts.map +1 -1
  55. package/dist/gateway.js +104 -14
  56. package/dist/gateway.js.map +1 -1
  57. package/dist/inbound-debounce.d.ts +4 -1
  58. package/dist/inbound-debounce.d.ts.map +1 -1
  59. package/dist/inbound-debounce.js +4 -2
  60. package/dist/inbound-debounce.js.map +1 -1
  61. package/dist/index.d.ts +3 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +3 -1
  64. package/dist/index.js.map +1 -1
  65. package/dist/runtime-builder.d.ts +9 -0
  66. package/dist/runtime-builder.d.ts.map +1 -1
  67. package/dist/runtime-builder.js +110 -0
  68. package/dist/runtime-builder.js.map +1 -1
  69. package/package.json +25 -8
@@ -0,0 +1,122 @@
1
+ /** The longest line read as an attribution: longer ones are text, whatever they say. */
2
+ const MAX_ATTRIBUTION = 500;
3
+ /**
4
+ * Lines that introduce a quoted message, as mail clients write them in several languages: how
5
+ * they start and the word that names the writing. Checked as two linear tests, not one pattern.
6
+ */
7
+ const ATTRIBUTIONS = [
8
+ [/^On\b/i, /\bwrote:$/i],
9
+ [/^Le\b/i, /\ba écrit\s?:$/i],
10
+ [/^Am\b/i, /\bschrieb\b/i],
11
+ [/^El\b/i, /\bescribió:$/i],
12
+ [/^Em\b/i, /\bescreveu:$/i],
13
+ [/^Op\b/i, /\bschreef\b/i],
14
+ [/^Il\b/i, /\bha scritto:$/i],
15
+ ];
16
+ function isAttribution(line) {
17
+ if (line.length > MAX_ATTRIBUTION || !line.endsWith(':'))
18
+ return false;
19
+ return ATTRIBUTIONS.some(([start, word]) => start.test(line) && word.test(line));
20
+ }
21
+ const ADDRESS_TOKEN = /^<?[^\s@<>]+@[^\s@<>.]+(?:\.[^\s@<>.]+)+>?,?$/;
22
+ const WROTE = /^(пишет|написал\(а\)|написала?|wrote)$/i;
23
+ /**
24
+ * A line that ends with an address and a colon, as Gmail and Yandex write in Russian
25
+ * (`пн, 19 окт. 2026 г. в 10:00, Иван <ivan@example.ru>:`), read token by token.
26
+ */
27
+ function isAddressAttribution(line) {
28
+ if (line.length > MAX_ATTRIBUTION || !line.endsWith(':'))
29
+ return false;
30
+ const words = line.slice(0, -1).trim().split(/\s+/);
31
+ let last = words.pop() ?? '';
32
+ if (WROTE.test(last))
33
+ last = words.pop() ?? '';
34
+ return ADDRESS_TOKEN.test(last);
35
+ }
36
+ const FORWARD_HEADER = /^(-{2,}\s*(Original Message|Ursprüngliche Nachricht|Исходное сообщение|Message d'origine)\s*-{2,}|_{10,})$/i;
37
+ const OUTLOOK_FROM = /^\*?(From|De|Von|От|Van|Da):\*?\s/i;
38
+ const OUTLOOK_FIELD = /^\*?(Sent|Date|To|Subject|Cc|Envoyé|Gesendet|Отправлено|Дата|Кому|Тема|Verzonden|Inviato|Enviado|Objet|Betreff|À|An|Aan|A|Para):\*?\s/i;
39
+ const MOBILE_SIGNATURE = /^(Sent from my \S+|Sent from (Mail|Outlook|Yahoo Mail) for \S+|Get Outlook for \S+|Отправлено с \S+)/i;
40
+ function isQuoted(line) {
41
+ return /^\s*>/.test(line);
42
+ }
43
+ function linesBelow(lines) {
44
+ const nextContent = new Array(lines.length + 1);
45
+ const onlyQuotes = new Array(lines.length + 1);
46
+ nextContent[lines.length] = lines.length;
47
+ onlyQuotes[lines.length] = false;
48
+ let clean = true;
49
+ let quoted = false;
50
+ for (let i = lines.length - 1; i >= 0; i--) {
51
+ const blank = !lines[i].trim();
52
+ nextContent[i] = blank ? nextContent[i + 1] : i;
53
+ if (!blank) {
54
+ if (isQuoted(lines[i]))
55
+ quoted = true;
56
+ else
57
+ clean = false;
58
+ }
59
+ onlyQuotes[i] = clean && quoted;
60
+ }
61
+ return { nextContent, onlyQuotes };
62
+ }
63
+ /** Where a quoted message starts, if it starts at line `i`: its attribution or separator. */
64
+ function quoteStartsAt(lines, i, below) {
65
+ const line = lines[i].trim();
66
+ if (!line)
67
+ return false;
68
+ if (FORWARD_HEADER.test(line))
69
+ return true;
70
+ const wrapped = i + 1 < lines.length && line.length <= MAX_ATTRIBUTION;
71
+ const joined = wrapped ? `${line} ${lines[i + 1].trim()}` : line;
72
+ const single = isAttribution(line);
73
+ if (single || (wrapped && isAttribution(joined))) {
74
+ const next = below.nextContent[single ? i + 1 : i + 2] ?? lines.length;
75
+ return next >= lines.length || isQuoted(lines[next]) || OUTLOOK_FROM.test(lines[next]);
76
+ }
77
+ if (isAddressAttribution(line)) {
78
+ const next = below.nextContent[i + 1];
79
+ return next < lines.length && below.onlyQuotes[next];
80
+ }
81
+ if (OUTLOOK_FROM.test(line)) {
82
+ let fields = 0;
83
+ for (let j = i + 1; j < Math.min(lines.length, i + 6); j++) {
84
+ if (OUTLOOK_FIELD.test(lines[j].trim()))
85
+ fields++;
86
+ }
87
+ return fields >= 2;
88
+ }
89
+ return false;
90
+ }
91
+ /**
92
+ * The new text of a reply, without what it quotes: the history after an attribution line
93
+ * (`On ... wrote:` and its translations, `-----Original Message-----`, an Outlook `From:` and
94
+ * `Sent:` block), a trailing block of `>` quotes, the signature after a `-- ` line and a
95
+ * `Sent from my ...` line. Quotes the writer answers between are kept, as they say what each
96
+ * answer is about. Returns the text unchanged when stripping would leave nothing.
97
+ */
98
+ export function stripQuotedReply(text) {
99
+ const lines = text.replace(/\r\n?/g, '\n').split('\n');
100
+ const below = linesBelow(lines);
101
+ let end = lines.length;
102
+ for (let i = 0; i < lines.length; i++) {
103
+ if (lines[i] === '-- ' || lines[i] === '--') {
104
+ end = i;
105
+ break;
106
+ }
107
+ if (quoteStartsAt(lines, i, below)) {
108
+ end = i;
109
+ break;
110
+ }
111
+ }
112
+ let kept = lines.slice(0, end);
113
+ while (kept.length > 0 && (!kept[kept.length - 1].trim() || isQuoted(kept[kept.length - 1]))) {
114
+ kept.pop();
115
+ }
116
+ const last = kept.length - 1;
117
+ if (last >= 0 && MOBILE_SIGNATURE.test(kept[last].trim()))
118
+ kept = kept.slice(0, last);
119
+ const stripped = kept.join('\n').trim();
120
+ return stripped || text.trim();
121
+ }
122
+ //# sourceMappingURL=reply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply.js","sourceRoot":"","sources":["../../../src/channels/email/reply.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /** Text with HTML entities (named, decimal and hex) decoded, unknown names staying as written. */
2
+ export declare function decodeHtmlEntities(text: string): string;
3
+ export declare function escapeHtml(text: string): string;
4
+ /**
5
+ * The text of an HTML email: paragraphs and line breaks kept, links written as `text (url)`,
6
+ * list items with a dash, quoted blocks (`<blockquote>`) as `>` lines, scripts, styles and the
7
+ * head dropped and entities decoded.
8
+ */
9
+ export declare function htmlToText(html: string): string;
10
+ /**
11
+ * Markdown as the HTML body of an email: headings, paragraphs, lists, quotes, code blocks,
12
+ * tables, emphasis and links (only `http`, `https` and `mailto`), with inline styles, since
13
+ * many mail clients drop style sheets. Raw HTML in the Markdown is escaped, not rendered.
14
+ */
15
+ export declare function markdownToHtml(markdown: string): string;
16
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/formatters/html.ts"],"names":[],"mappings":"AA2BA,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWvD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO/C;AAwHD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuJ/C;AAqOD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGvD"}
@@ -0,0 +1,535 @@
1
+ const NAMED_ENTITIES = {
2
+ amp: '&',
3
+ lt: '<',
4
+ gt: '>',
5
+ quot: '"',
6
+ apos: "'",
7
+ nbsp: ' ',
8
+ copy: '\u00A9',
9
+ reg: '\u00AE',
10
+ trade: '\u2122',
11
+ hellip: '\u2026',
12
+ mdash: '\u2014',
13
+ ndash: '\u2013',
14
+ lsquo: '\u2018',
15
+ rsquo: '\u2019',
16
+ ldquo: '\u201C',
17
+ rdquo: '\u201D',
18
+ laquo: '\u00AB',
19
+ raquo: '\u00BB',
20
+ bull: '\u2022',
21
+ middot: '\u00B7',
22
+ euro: '\u20AC',
23
+ zwnj: '',
24
+ zwj: '',
25
+ shy: '',
26
+ };
27
+ /** Text with HTML entities (named, decimal and hex) decoded, unknown names staying as written. */
28
+ export function decodeHtmlEntities(text) {
29
+ return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (entity, name) => {
30
+ if (name.startsWith('#')) {
31
+ const code = name[1] === 'x' || name[1] === 'X' ? parseInt(name.slice(2), 16) : Number(name.slice(1));
32
+ return Number.isFinite(code) && code > 0 && code <= 0x10ffff
33
+ ? String.fromCodePoint(code)
34
+ : entity;
35
+ }
36
+ return NAMED_ENTITIES[name.toLowerCase()] ?? entity;
37
+ });
38
+ }
39
+ export function escapeHtml(text) {
40
+ return text
41
+ .replace(/&/g, '&amp;')
42
+ .replace(/</g, '&lt;')
43
+ .replace(/>/g, '&gt;')
44
+ .replace(/"/g, '&quot;')
45
+ .replace(/'/g, '&#39;');
46
+ }
47
+ const SKIPPED = new Set(['script', 'style', 'head', 'title', 'template', 'noscript']);
48
+ const PARAGRAPHS = new Set([
49
+ 'p',
50
+ 'h1',
51
+ 'h2',
52
+ 'h3',
53
+ 'h4',
54
+ 'h5',
55
+ 'h6',
56
+ 'ul',
57
+ 'ol',
58
+ 'table',
59
+ 'pre',
60
+ 'dl',
61
+ 'figure',
62
+ ]);
63
+ const LINES = new Set([
64
+ 'div',
65
+ 'tr',
66
+ 'li',
67
+ 'section',
68
+ 'article',
69
+ 'header',
70
+ 'footer',
71
+ 'address',
72
+ 'dt',
73
+ 'dd',
74
+ 'caption',
75
+ 'center',
76
+ ]);
77
+ function attribute(attributes, name) {
78
+ const match = new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i').exec(attributes);
79
+ const value = match?.[1] ?? match?.[2] ?? match?.[3];
80
+ return value === undefined ? undefined : decodeHtmlEntities(value);
81
+ }
82
+ /** `text` without the trailing characters of `chars`, in one pass from the end. */
83
+ function trimEndOf(text, chars) {
84
+ let end = text.length;
85
+ while (end > 0 && chars.includes(text[end - 1]))
86
+ end--;
87
+ return text.slice(0, end);
88
+ }
89
+ const TAG_NAME = /\/?([a-zA-Z][\w:-]*)/y;
90
+ /**
91
+ * The tokens of HTML, in one linear pass: text, and tags with their attributes. Comments,
92
+ * CDATA, doctypes and processing instructions are left out. A tag that is never closed makes
93
+ * the rest of the input text, and a comment that is never closed drops it, so malformed input
94
+ * cannot make the scan go over the rest again for every `<`.
95
+ */
96
+ function* htmlTokens(html) {
97
+ let i = 0;
98
+ const skipTo = (marker, from) => {
99
+ const end = html.indexOf(marker, from);
100
+ return end === -1 ? html.length : end + marker.length;
101
+ };
102
+ while (i < html.length) {
103
+ const open = html.indexOf('<', i);
104
+ if (open === -1) {
105
+ yield { text: html.slice(i) };
106
+ return;
107
+ }
108
+ if (open > i)
109
+ yield { text: html.slice(i, open) };
110
+ if (html.startsWith('<!--', open)) {
111
+ i = skipTo('-->', open + 4);
112
+ continue;
113
+ }
114
+ if (html.startsWith('<![CDATA[', open)) {
115
+ i = skipTo(']]>', open + 9);
116
+ continue;
117
+ }
118
+ if (html[open + 1] === '!' || html[open + 1] === '?') {
119
+ i = skipTo('>', open + 2);
120
+ continue;
121
+ }
122
+ TAG_NAME.lastIndex = open + 1;
123
+ const name = TAG_NAME.exec(html);
124
+ if (!name) {
125
+ yield { text: '<' };
126
+ i = open + 1;
127
+ continue;
128
+ }
129
+ const attributesStart = TAG_NAME.lastIndex;
130
+ let end = attributesStart;
131
+ let quote = '';
132
+ let previous = '';
133
+ for (; end < html.length; end++) {
134
+ const char = html[end];
135
+ if (quote) {
136
+ if (char === quote)
137
+ quote = '';
138
+ }
139
+ else if ((char === '"' || char === "'") && previous === '=')
140
+ quote = char;
141
+ else if (char === '>')
142
+ break;
143
+ if (!/\s/.test(char))
144
+ previous = char;
145
+ }
146
+ if (end >= html.length) {
147
+ yield { text: html.slice(open) };
148
+ return;
149
+ }
150
+ yield {
151
+ closing: name[0].startsWith('/'),
152
+ name: name[1],
153
+ attributes: html.slice(attributesStart, end),
154
+ };
155
+ i = end + 1;
156
+ }
157
+ }
158
+ /**
159
+ * The text of an HTML email: paragraphs and line breaks kept, links written as `text (url)`,
160
+ * list items with a dash, quoted blocks (`<blockquote>`) as `>` lines, scripts, styles and the
161
+ * head dropped and entities decoded.
162
+ */
163
+ export function htmlToText(html) {
164
+ const lines = [];
165
+ let line = '';
166
+ let depth = 0;
167
+ let lineDepth = 0;
168
+ let skipping = 0;
169
+ let pre = 0;
170
+ const lists = [];
171
+ const links = [];
172
+ const flush = () => {
173
+ lines.push({ depth: lineDepth, text: trimEndOf(line, ' \t') });
174
+ line = '';
175
+ lineDepth = depth;
176
+ };
177
+ const endLine = () => {
178
+ if (line.trim())
179
+ flush();
180
+ else {
181
+ line = '';
182
+ lineDepth = depth;
183
+ }
184
+ };
185
+ const endParagraph = () => {
186
+ endLine();
187
+ const last = lines[lines.length - 1];
188
+ if (last && last.text !== '')
189
+ lines.push({ depth: last.depth, text: '' });
190
+ };
191
+ const write = (text) => {
192
+ if (!line)
193
+ lineDepth = depth;
194
+ line += text;
195
+ };
196
+ for (const token of htmlTokens(html)) {
197
+ if ('text' in token) {
198
+ if (skipping > 0)
199
+ continue;
200
+ const decoded = decodeHtmlEntities(token.text);
201
+ if (pre > 0) {
202
+ const parts = decoded.split('\n');
203
+ parts.forEach((part, index) => {
204
+ if (index > 0)
205
+ flush();
206
+ write(part);
207
+ });
208
+ }
209
+ else {
210
+ const collapsed = decoded.replace(/\s+/g, ' ');
211
+ write(line === '' || line.endsWith(' ') ? collapsed.replace(/^ /, '') : collapsed);
212
+ }
213
+ continue;
214
+ }
215
+ const { closing, attributes } = token;
216
+ const name = token.name.toLowerCase();
217
+ if (SKIPPED.has(name)) {
218
+ if (closing)
219
+ skipping = Math.max(0, skipping - 1);
220
+ else if (!attributes.trimEnd().endsWith('/'))
221
+ skipping++;
222
+ continue;
223
+ }
224
+ if (skipping > 0)
225
+ continue;
226
+ if (closing) {
227
+ switch (name) {
228
+ case 'blockquote':
229
+ endParagraph();
230
+ depth = Math.max(0, depth - 1);
231
+ lineDepth = depth;
232
+ break;
233
+ case 'pre':
234
+ pre = Math.max(0, pre - 1);
235
+ endParagraph();
236
+ break;
237
+ case 'ul':
238
+ case 'ol':
239
+ lists.pop();
240
+ endParagraph();
241
+ break;
242
+ case 'a': {
243
+ const link = links.pop();
244
+ const label = link ? line.slice(link.start).trim() : '';
245
+ if (link?.href &&
246
+ /^https?:\/\//i.test(link.href) &&
247
+ label !== link.href &&
248
+ label !== link.href.replace(/^https?:\/\//i, '').replace(/\/$/, '')) {
249
+ write(label ? ` (${link.href})` : link.href);
250
+ }
251
+ break;
252
+ }
253
+ case 'td':
254
+ case 'th':
255
+ write('\t');
256
+ break;
257
+ default:
258
+ if (PARAGRAPHS.has(name))
259
+ endParagraph();
260
+ else if (LINES.has(name))
261
+ endLine();
262
+ }
263
+ continue;
264
+ }
265
+ switch (name) {
266
+ case 'br':
267
+ flush();
268
+ break;
269
+ case 'hr':
270
+ endLine();
271
+ write('---');
272
+ endLine();
273
+ break;
274
+ case 'img': {
275
+ const alt = attribute(attributes, 'alt');
276
+ if (alt)
277
+ write(alt);
278
+ break;
279
+ }
280
+ case 'blockquote':
281
+ endParagraph();
282
+ depth++;
283
+ lineDepth = depth;
284
+ break;
285
+ case 'pre':
286
+ endParagraph();
287
+ pre++;
288
+ break;
289
+ case 'ul':
290
+ case 'ol':
291
+ endLine();
292
+ lists.push({ ordered: name === 'ol', next: Number(attribute(attributes, 'start')) || 1 });
293
+ break;
294
+ case 'li': {
295
+ endLine();
296
+ const list = lists[lists.length - 1];
297
+ const indent = ' '.repeat(Math.max(0, lists.length - 1));
298
+ write(list?.ordered ? `${indent}${list.next++}. ` : `${indent}- `);
299
+ break;
300
+ }
301
+ case 'a':
302
+ links.push({ href: attribute(attributes, 'href'), start: line.length });
303
+ break;
304
+ default:
305
+ if (PARAGRAPHS.has(name))
306
+ endParagraph();
307
+ else if (LINES.has(name))
308
+ endLine();
309
+ }
310
+ }
311
+ endLine();
312
+ const rendered = lines.map(({ depth: level, text }) => {
313
+ const cleaned = trimEndOf(text, '\t').replace(/\t/g, ' | ');
314
+ return level > 0 ? `${'> '.repeat(level)}${cleaned}`.trimEnd() : cleaned;
315
+ });
316
+ while (rendered.length > 0 && trimEndOf(rendered[rendered.length - 1], '> ') === '') {
317
+ rendered.pop();
318
+ }
319
+ return rendered
320
+ .join('\n')
321
+ .replace(/\n{3,}/g, '\n\n')
322
+ .trim();
323
+ }
324
+ const CODE_STYLE = 'font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: #f4f4f5; border-radius: 4px';
325
+ function safeUrl(url) {
326
+ const decoded = decodeHtmlEntities(url).trim();
327
+ return /^(https?:|mailto:)/i.test(decoded) ? escapeHtml(decoded) : undefined;
328
+ }
329
+ /** How deep quotes and lists nest before the rest is a paragraph of text. */
330
+ const MAX_NESTING = 16;
331
+ /** The longest text formatted inline: a longer paragraph is sent as escaped text. */
332
+ const MAX_INLINE = 100_000;
333
+ /** Inline Markdown (code spans, emphasis, links) as HTML, the text escaped. */
334
+ function inlineHtml(input) {
335
+ if (input.length > MAX_INLINE)
336
+ return escapeHtml(input).replace(/\n/g, '<br>\n');
337
+ const code = [];
338
+ const keep = (html) => `\uE000${code.push(html) - 1}\uE000`;
339
+ const text = input.replace(/\uE000/g, '\uFFFD');
340
+ let html = text.replace(/(`+)([\s\S]{0,4000}?[^`])\1(?!`)/g, (_, _ticks, body) => keep(`<code style="${CODE_STYLE}; padding: 0 3px">${escapeHtml(body.trim())}</code>`));
341
+ html = escapeHtml(html)
342
+ .replace(/!\[([^\]\n]{0,1000})\]\(([^)\s]{1,2048})(?:\s+&quot;[^)\n]{0,1000}&quot;)?\)/g, (whole, alt, url) => {
343
+ const href = safeUrl(url);
344
+ return href ? keep(`<a href="${href}">${alt || href}</a>`) : whole;
345
+ })
346
+ .replace(/\[([^\]\n]{1,1000})\]\(([^)\s]{1,2048})(?:\s+&quot;[^)\n]{0,1000}&quot;)?\)/g, (whole, label, url) => {
347
+ const href = safeUrl(url);
348
+ return href ? keep(`<a href="${href}">${label}</a>`) : whole;
349
+ })
350
+ .replace(/&lt;((?:https?:\/\/|mailto:)[^\s&]{1,2048}(?:&amp;[^\s&]{1,2048}){0,64})&gt;/gi, (whole, url) => {
351
+ const href = safeUrl(url);
352
+ return href ? keep(`<a href="${href}">${url}</a>`) : whole;
353
+ })
354
+ .replace(/(\*\*|__)(?=\S)([^\n]{0,2000}?\S)\1/g, '<strong>$2</strong>')
355
+ .replace(/(?<![\w*])\*(?=\S)([^*\n]*?\S)\*(?![\w*])/g, '<em>$1</em>')
356
+ .replace(/(?<![\w_])_(?=\S)([^_\n]*?\S)_(?![\w_])/g, '<em>$1</em>')
357
+ .replace(/~~(?=\S)([^\n]{0,2000}?\S)~~/g, '<del>$1</del>')
358
+ .replace(/\n/g, '<br>\n');
359
+ for (let round = 0; round < 3 && html.includes('\uE000'); round++) {
360
+ html = html.replace(/\uE000(\d+)\uE000/g, (_, index) => code[Number(index)]);
361
+ }
362
+ return html;
363
+ }
364
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
365
+ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
366
+ const RULE = /^ {0,3}([-*_])(\s*\1){2,}\s*$/;
367
+ const QUOTE = /^ {0,3}>\s?(.*)$/;
368
+ const LIST_ITEM = /^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$/;
369
+ const TABLE_SEPARATOR = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/;
370
+ function tableCells(line) {
371
+ return line
372
+ .trim()
373
+ .replace(/^\|/, '')
374
+ .replace(/\|$/, '')
375
+ .split(/(?<!\\)\|/)
376
+ .map((cell) => cell.trim().replace(/\\\|/g, '|'));
377
+ }
378
+ function startsBlock(line, next) {
379
+ return (FENCE_OPEN.test(line) ||
380
+ HEADING.test(line) ||
381
+ RULE.test(line) ||
382
+ QUOTE.test(line) ||
383
+ LIST_ITEM.test(line) ||
384
+ (line.includes('|') && next !== undefined && TABLE_SEPARATOR.test(next) && next.includes('-')));
385
+ }
386
+ function renderList(lines, depth) {
387
+ const first = LIST_ITEM.exec(lines[0]);
388
+ const indent = first?.[1].length ?? 0;
389
+ const ordered = /\d/.test(first?.[2] ?? '');
390
+ const items = [];
391
+ for (const line of lines) {
392
+ const item = LIST_ITEM.exec(line);
393
+ if (item && item[1].length <= indent)
394
+ items.push([item[3]]);
395
+ else if (items.length > 0)
396
+ items[items.length - 1].push(line.replace(/^\s{1,4}/, ''));
397
+ }
398
+ const start = ordered ? Number(/\d+/.exec(first?.[2] ?? '1')?.[0] ?? 1) : 1;
399
+ const tag = ordered ? 'ol' : 'ul';
400
+ const body = items
401
+ .map((item) => {
402
+ const nested = item.findIndex((line, index) => index > 0 && LIST_ITEM.test(line));
403
+ if (nested === -1 && !item.some((line) => !line.trim())) {
404
+ return `<li>${inlineHtml(item.join('\n').trim())}</li>`;
405
+ }
406
+ const head = nested === -1 ? item : item.slice(0, nested);
407
+ const rest = nested === -1 ? [] : item.slice(nested);
408
+ return `<li>${inlineHtml(head.join('\n').trim())}${rest.length ? renderBlocks(rest, depth + 1) : ''}</li>`;
409
+ })
410
+ .join('');
411
+ return `<${tag}${ordered && start !== 1 ? ` start="${start}"` : ''}>${body}</${tag}>`;
412
+ }
413
+ function renderBlocks(lines, depth = 0) {
414
+ if (depth >= MAX_NESTING) {
415
+ return `<p>${inlineHtml(lines.join('\n').trim())}</p>`;
416
+ }
417
+ const out = [];
418
+ let i = 0;
419
+ while (i < lines.length) {
420
+ const line = lines[i];
421
+ if (!line.trim()) {
422
+ i++;
423
+ continue;
424
+ }
425
+ const fence = FENCE_OPEN.exec(line);
426
+ if (fence) {
427
+ const marker = fence[1];
428
+ const body = [];
429
+ i++;
430
+ while (i < lines.length) {
431
+ const closing = /^ {0,3}(`{3,}|~{3,})\s*$/.exec(lines[i]);
432
+ if (closing?.[1].startsWith(marker[0]) && closing[1].length >= marker.length)
433
+ break;
434
+ body.push(lines[i]);
435
+ i++;
436
+ }
437
+ i++;
438
+ out.push(`<pre style="${CODE_STYLE}; padding: 8px 12px; overflow-x: auto"><code>${escapeHtml(body.join('\n'))}</code></pre>`);
439
+ continue;
440
+ }
441
+ const heading = HEADING.exec(line);
442
+ if (heading) {
443
+ const level = heading[1].length;
444
+ out.push(`<h${level}>${inlineHtml(heading[2])}</h${level}>`);
445
+ i++;
446
+ continue;
447
+ }
448
+ if (RULE.test(line)) {
449
+ out.push('<hr>');
450
+ i++;
451
+ continue;
452
+ }
453
+ if (QUOTE.test(line)) {
454
+ const quoted = [];
455
+ while (i < lines.length && lines[i].trim()) {
456
+ const match = QUOTE.exec(lines[i]);
457
+ quoted.push(match ? match[1] : lines[i]);
458
+ i++;
459
+ }
460
+ out.push(`<blockquote style="margin: 0 0 0 8px; padding-left: 12px; border-left: 3px solid #d4d4d8; color: #52525b">${renderBlocks(quoted, depth + 1)}</blockquote>`);
461
+ continue;
462
+ }
463
+ const listStart = LIST_ITEM.exec(line);
464
+ if (listStart) {
465
+ const indent = listStart[1].length;
466
+ const ordered = /\d/.test(listStart[2]);
467
+ const sameList = (candidate) => {
468
+ const item = LIST_ITEM.exec(candidate);
469
+ if (!item)
470
+ return false;
471
+ return item[1].length > indent || /\d/.test(item[2]) === ordered;
472
+ };
473
+ const items = [];
474
+ while (i < lines.length) {
475
+ const current = lines[i];
476
+ if (LIST_ITEM.test(current) && !sameList(current))
477
+ break;
478
+ if (!current.trim()) {
479
+ const next = lines[i + 1];
480
+ if (next !== undefined && (sameList(next) || /^\s{2,}\S/.test(next))) {
481
+ items.push(current);
482
+ i++;
483
+ continue;
484
+ }
485
+ break;
486
+ }
487
+ if (!LIST_ITEM.test(current) && !/^\s/.test(current) && items.length > 0) {
488
+ const previous = items[items.length - 1];
489
+ if (!previous.trim() || startsBlock(current, lines[i + 1]))
490
+ break;
491
+ }
492
+ items.push(current);
493
+ i++;
494
+ }
495
+ out.push(renderList(items, depth));
496
+ continue;
497
+ }
498
+ const next = lines[i + 1];
499
+ if (line.includes('|') &&
500
+ next !== undefined &&
501
+ TABLE_SEPARATOR.test(next) &&
502
+ next.includes('-')) {
503
+ const header = tableCells(line);
504
+ const rows = [];
505
+ i += 2;
506
+ while (i < lines.length && lines[i].includes('|') && lines[i].trim()) {
507
+ rows.push(tableCells(lines[i]));
508
+ i++;
509
+ }
510
+ const cell = (tag, text) => `<${tag} style="border: 1px solid #d4d4d8; padding: 4px 8px; text-align: left">${inlineHtml(text)}</${tag}>`;
511
+ out.push(`<table style="border-collapse: collapse"><thead><tr>${header.map((text) => cell('th', text)).join('')}</tr></thead><tbody>${rows
512
+ .map((row) => `<tr>${row.map((text) => cell('td', text)).join('')}</tr>`)
513
+ .join('')}</tbody></table>`);
514
+ continue;
515
+ }
516
+ const paragraph = [line];
517
+ i++;
518
+ while (i < lines.length && lines[i].trim() && !startsBlock(lines[i], lines[i + 1])) {
519
+ paragraph.push(lines[i]);
520
+ i++;
521
+ }
522
+ out.push(`<p>${inlineHtml(paragraph.join('\n').trim())}</p>`);
523
+ }
524
+ return out.join('\n');
525
+ }
526
+ /**
527
+ * Markdown as the HTML body of an email: headings, paragraphs, lists, quotes, code blocks,
528
+ * tables, emphasis and links (only `http`, `https` and `mailto`), with inline styles, since
529
+ * many mail clients drop style sheets. Raw HTML in the Markdown is escaped, not rendered.
530
+ */
531
+ export function markdownToHtml(markdown) {
532
+ const body = renderBlocks(markdown.replace(/\r\n?/g, '\n').split('\n'));
533
+ return `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #18181b">\n${body}\n</div>`;
534
+ }
535
+ //# sourceMappingURL=html.js.map