@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.
- package/README.md +53 -12
- package/dist/assistant-config.d.ts +40 -0
- package/dist/assistant-config.d.ts.map +1 -1
- package/dist/assistant-config.js +66 -0
- package/dist/assistant-config.js.map +1 -1
- package/dist/channels/email/address.d.ts +44 -0
- package/dist/channels/email/address.d.ts.map +1 -0
- package/dist/channels/email/address.js +211 -0
- package/dist/channels/email/address.js.map +1 -0
- package/dist/channels/email/auth.d.ts +59 -0
- package/dist/channels/email/auth.d.ts.map +1 -0
- package/dist/channels/email/auth.js +212 -0
- package/dist/channels/email/auth.js.map +1 -0
- package/dist/channels/email/channel.d.ts +256 -0
- package/dist/channels/email/channel.d.ts.map +1 -0
- package/dist/channels/email/channel.js +701 -0
- package/dist/channels/email/channel.js.map +1 -0
- package/dist/channels/email/errors.d.ts +11 -0
- package/dist/channels/email/errors.d.ts.map +1 -0
- package/dist/channels/email/errors.js +13 -0
- package/dist/channels/email/errors.js.map +1 -0
- package/dist/channels/email/imap.d.ts +125 -0
- package/dist/channels/email/imap.d.ts.map +1 -0
- package/dist/channels/email/imap.js +309 -0
- package/dist/channels/email/imap.js.map +1 -0
- package/dist/channels/email/index.d.ts +7 -0
- package/dist/channels/email/index.d.ts.map +1 -0
- package/dist/channels/email/index.js +3 -0
- package/dist/channels/email/index.js.map +1 -0
- package/dist/channels/email/mime.d.ts +71 -0
- package/dist/channels/email/mime.d.ts.map +1 -0
- package/dist/channels/email/mime.js +83 -0
- package/dist/channels/email/mime.js.map +1 -0
- package/dist/channels/email/multipart.d.ts +28 -0
- package/dist/channels/email/multipart.d.ts.map +1 -0
- package/dist/channels/email/multipart.js +102 -0
- package/dist/channels/email/multipart.js.map +1 -0
- package/dist/channels/email/providers.d.ts +49 -0
- package/dist/channels/email/providers.d.ts.map +1 -0
- package/dist/channels/email/providers.js +292 -0
- package/dist/channels/email/providers.js.map +1 -0
- package/dist/channels/email/reply.d.ts +9 -0
- package/dist/channels/email/reply.d.ts.map +1 -0
- package/dist/channels/email/reply.js +122 -0
- package/dist/channels/email/reply.js.map +1 -0
- package/dist/formatters/html.d.ts +16 -0
- package/dist/formatters/html.d.ts.map +1 -0
- package/dist/formatters/html.js +535 -0
- package/dist/formatters/html.js.map +1 -0
- package/dist/formatters/markdown.d.ts.map +1 -1
- package/dist/formatters/markdown.js +1 -0
- package/dist/formatters/markdown.js.map +1 -1
- package/dist/gateway.d.ts +23 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +104 -14
- package/dist/gateway.js.map +1 -1
- package/dist/inbound-debounce.d.ts +4 -1
- package/dist/inbound-debounce.d.ts.map +1 -1
- package/dist/inbound-debounce.js +4 -2
- package/dist/inbound-debounce.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime-builder.d.ts +9 -0
- package/dist/runtime-builder.d.ts.map +1 -1
- package/dist/runtime-builder.js +110 -0
- package/dist/runtime-builder.js.map +1 -1
- 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, '&')
|
|
42
|
+
.replace(/</g, '<')
|
|
43
|
+
.replace(/>/g, '>')
|
|
44
|
+
.replace(/"/g, '"')
|
|
45
|
+
.replace(/'/g, ''');
|
|
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+"[^)\n]{0,1000}")?\)/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+"[^)\n]{0,1000}")?\)/g, (whole, label, url) => {
|
|
347
|
+
const href = safeUrl(url);
|
|
348
|
+
return href ? keep(`<a href="${href}">${label}</a>`) : whole;
|
|
349
|
+
})
|
|
350
|
+
.replace(/<((?:https?:\/\/|mailto:)[^\s&]{1,2048}(?:&[^\s&]{1,2048}){0,64})>/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
|