@handled-ai/design-system 0.20.5 → 0.20.7
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/dist/components/conversation-panel.d.ts +19 -0
- package/dist/components/conversation-panel.js +116 -292
- package/dist/components/conversation-panel.js.map +1 -1
- package/dist/components/email-body.d.ts +15 -0
- package/dist/components/email-body.js +101 -0
- package/dist/components/email-body.js.map +1 -0
- package/dist/components/email-display-helpers.d.ts +34 -0
- package/dist/components/email-display-helpers.js +436 -0
- package/dist/components/email-display-helpers.js.map +1 -0
- package/dist/components/email-preview-card.d.ts +7 -4
- package/dist/components/email-preview-card.js +48 -25
- package/dist/components/email-preview-card.js.map +1 -1
- package/dist/components/timeline-activity.d.ts +1 -0
- package/dist/components/timeline-activity.js +116 -65
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/safe-html.d.ts +1 -1
- package/dist/internal/safe-html.js +64 -3
- package/dist/internal/safe-html.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/conversation-panel.test.tsx +182 -22
- package/src/components/__tests__/email-body.test.tsx +83 -0
- package/src/components/__tests__/email-display-helpers.test.ts +91 -0
- package/src/components/__tests__/email-preview-card.test.tsx +36 -2
- package/src/components/__tests__/timeline-activity.test.tsx +87 -1
- package/src/components/conversation-panel.tsx +136 -350
- package/src/components/email-body.tsx +126 -0
- package/src/components/email-display-helpers.ts +557 -0
- package/src/components/email-preview-card.tsx +54 -29
- package/src/components/timeline-activity.tsx +105 -63
- package/src/index.ts +2 -0
- package/src/internal/__tests__/safe-html.test.ts +34 -2
- package/src/internal/safe-html.ts +79 -4
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { htmlToTextSnippet, sanitizeHtml } from "../internal/safe-html.js";
|
|
2
|
+
const HTML_ENTITY_RE = /&(?:#x[0-9a-f]+|#\d+|[a-z][a-z0-9]+);?/i;
|
|
3
|
+
const EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i;
|
|
4
|
+
const ANGLE_ADDRESS_RE = /^\s*(.*?)\s*<\s*([^<>\s]+@[^<>\s]+)\s*>\s*$/;
|
|
5
|
+
const BR_TAG_RE = /<br\s*\/?>/gi;
|
|
6
|
+
const SPLITTABLE_BLOCK_TAGS = /* @__PURE__ */ new Set(["p", "div"]);
|
|
7
|
+
const WHOLE_BLOCK_TAGS = /* @__PURE__ */ new Set(["blockquote", "table", "ul", "ol", "hr"]);
|
|
8
|
+
const BLOCK_TAGS = /* @__PURE__ */ new Set([...SPLITTABLE_BLOCK_TAGS, ...WHOLE_BLOCK_TAGS]);
|
|
9
|
+
const HTML_BLOCK_START_RE = /<(p|div|blockquote|table|ul|ol|hr)\b[^>]*>/i;
|
|
10
|
+
const SIGNATURE_DELIMITER_RE = /^--\s*$/;
|
|
11
|
+
const GMAIL_SIGNATURE_RE = /\b(?:gmail_signature|gmail_signature_prefix|gmail_extra)\b/i;
|
|
12
|
+
const GMAIL_QUOTE_RE = /<blockquote\b[^>]*\bclass=["'][^"']*\bgmail_quote\b/i;
|
|
13
|
+
const ON_WROTE_RE = /^On\s.+wrote:\s*$/i;
|
|
14
|
+
const SIGNOFF_RE = /^(?:thanks,|thank you,|best,|regards,|sincerely,)$/i;
|
|
15
|
+
const DETAILS_START_RE = /^(?:confidentiality notice\b|this message and any attachments\b|this email and any attachments\b|the information contained in this message\b|this communication may contain\b|unsubscribe\b|manage your preferences\b)/i;
|
|
16
|
+
const CONTACT_DETAIL_RE = /(?:@|https?:\/\/|www\.|\+?\d[\d\s().-]{6,}\d|\b(?:ceo|cfo|cto|coo|founder|co-founder|director|manager|vp|vice president|president|head of|sales|marketing|operations|account|customer success|success|support|engineer|consultant|partner|principal|advisor|associate)\b|\b(?:inc|llc|ltd|corp|corporation|company|co\.)\b)/i;
|
|
17
|
+
function safeCodePoint(value) {
|
|
18
|
+
return Number.isInteger(value) && value >= 0 && value <= 1114111 ? String.fromCodePoint(value) : "";
|
|
19
|
+
}
|
|
20
|
+
function decodeHtmlEntities(value) {
|
|
21
|
+
const namedEntities = {
|
|
22
|
+
amp: "&",
|
|
23
|
+
apos: "'",
|
|
24
|
+
colon: ":",
|
|
25
|
+
gt: ">",
|
|
26
|
+
lt: "<",
|
|
27
|
+
nbsp: " ",
|
|
28
|
+
newline: "\n",
|
|
29
|
+
quot: '"',
|
|
30
|
+
tab: " "
|
|
31
|
+
};
|
|
32
|
+
let decoded = value;
|
|
33
|
+
for (let i = 0; i < 4; i += 1) {
|
|
34
|
+
const next = decoded.replace(/&#x([0-9a-f]+);?/gi, (_match, hex) => safeCodePoint(Number.parseInt(hex, 16))).replace(/&#(\d+);?/g, (_match, decimal) => safeCodePoint(Number.parseInt(decimal, 10))).replace(/&([a-z][a-z0-9]+);?/gi, (match, name) => {
|
|
35
|
+
var _a;
|
|
36
|
+
return (_a = namedEntities[name.toLowerCase()]) != null ? _a : match;
|
|
37
|
+
});
|
|
38
|
+
if (next === decoded) return decoded;
|
|
39
|
+
decoded = next;
|
|
40
|
+
}
|
|
41
|
+
return decoded;
|
|
42
|
+
}
|
|
43
|
+
function decodeJsonEscapes(value) {
|
|
44
|
+
return value.replace(/\\u\{([0-9a-f]{1,6})\}/gi, (_match, hex) => safeCodePoint(Number.parseInt(hex, 16))).replace(/\\u([0-9a-f]{4})/gi, (_match, hex) => safeCodePoint(Number.parseInt(hex, 16))).replace(/\\r\\n|\\n|\\r/g, "\n").replace(/\\t/g, " ").replace(/\\(["'\\/])/g, "$1");
|
|
45
|
+
}
|
|
46
|
+
function maybeParseJsonString(value) {
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) return value;
|
|
49
|
+
if (!/[\\&]/.test(trimmed)) return value;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(trimmed);
|
|
52
|
+
return typeof parsed === "string" ? parsed : value;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function decodeEmailDisplayText(value) {
|
|
58
|
+
let decoded = maybeParseJsonString(value).replace(/\r\n?/g, "\n");
|
|
59
|
+
for (let i = 0; i < 4; i += 1) {
|
|
60
|
+
const next = decodeHtmlEntities(decodeJsonEscapes(decoded));
|
|
61
|
+
if (next === decoded) break;
|
|
62
|
+
decoded = next;
|
|
63
|
+
}
|
|
64
|
+
return decoded.replace(/\u00a0/g, " ");
|
|
65
|
+
}
|
|
66
|
+
function stripWrappingQuotes(value) {
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
69
|
+
return trimmed.slice(1, -1).trim();
|
|
70
|
+
}
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
function extractEmail(value) {
|
|
74
|
+
var _a, _b;
|
|
75
|
+
const decoded = decodeEmailDisplayText(value);
|
|
76
|
+
const angleMatch = decoded.match(ANGLE_ADDRESS_RE);
|
|
77
|
+
const email = (_b = angleMatch == null ? void 0 : angleMatch[2]) != null ? _b : (_a = decoded.match(EMAIL_RE)) == null ? void 0 : _a[0];
|
|
78
|
+
return email ? email.trim() : null;
|
|
79
|
+
}
|
|
80
|
+
function extractNameFromAddress(value) {
|
|
81
|
+
var _a;
|
|
82
|
+
const decoded = decodeEmailDisplayText(value);
|
|
83
|
+
const angleMatch = decoded.match(ANGLE_ADDRESS_RE);
|
|
84
|
+
if (angleMatch) return stripWrappingQuotes((_a = angleMatch[1]) != null ? _a : "");
|
|
85
|
+
return stripWrappingQuotes(decoded.replace(EMAIL_RE, "").replace(/[<>]/g, "").trim());
|
|
86
|
+
}
|
|
87
|
+
function cleanDisplayName(value, email) {
|
|
88
|
+
let name = stripWrappingQuotes(decodeEmailDisplayText(value));
|
|
89
|
+
if (email) {
|
|
90
|
+
name = name.replace(new RegExp(`<\\s*${email.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*>`, "i"), "").replace(new RegExp(`^${email.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "i"), "").trim();
|
|
91
|
+
}
|
|
92
|
+
return stripWrappingQuotes(name);
|
|
93
|
+
}
|
|
94
|
+
function normalizeEmailSender(input) {
|
|
95
|
+
var _a, _b;
|
|
96
|
+
const fallbackName = decodeEmailDisplayText((_a = input.fallbackName) != null ? _a : "Unknown sender").trim() || "Unknown sender";
|
|
97
|
+
const rawName = input.name ? decodeEmailDisplayText(input.name) : "";
|
|
98
|
+
const rawEmail = input.email ? decodeEmailDisplayText(input.email) : "";
|
|
99
|
+
const email = (_b = extractEmail(rawEmail)) != null ? _b : extractEmail(rawName);
|
|
100
|
+
const nameFromName = rawName ? cleanDisplayName(extractNameFromAddress(rawName) || rawName, email) : "";
|
|
101
|
+
const nameFromEmail = rawEmail ? cleanDisplayName(extractNameFromAddress(rawEmail), email) : "";
|
|
102
|
+
const name = nameFromName || nameFromEmail || email || fallbackName;
|
|
103
|
+
return { name, email };
|
|
104
|
+
}
|
|
105
|
+
function splitAddressList(value) {
|
|
106
|
+
const parts = [];
|
|
107
|
+
let current = "";
|
|
108
|
+
let quote = null;
|
|
109
|
+
let angleDepth = 0;
|
|
110
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
111
|
+
const char = value[index];
|
|
112
|
+
if (quote) {
|
|
113
|
+
current += char;
|
|
114
|
+
if (char === quote && value[index - 1] !== "\\") quote = null;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === '"' || char === "'") {
|
|
118
|
+
quote = char;
|
|
119
|
+
current += char;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (char === "<") angleDepth += 1;
|
|
123
|
+
if (char === ">" && angleDepth > 0) angleDepth -= 1;
|
|
124
|
+
if (char === "," && angleDepth === 0) {
|
|
125
|
+
if (current.trim()) parts.push(current.trim());
|
|
126
|
+
current = "";
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
current += char;
|
|
130
|
+
}
|
|
131
|
+
if (current.trim()) parts.push(current.trim());
|
|
132
|
+
return parts;
|
|
133
|
+
}
|
|
134
|
+
function formatSingleAddress(value) {
|
|
135
|
+
const decoded = decodeEmailDisplayText(value).trim();
|
|
136
|
+
if (!decoded) return "";
|
|
137
|
+
const email = extractEmail(decoded);
|
|
138
|
+
const name = cleanDisplayName(extractNameFromAddress(decoded), email);
|
|
139
|
+
if (email && name) return `${name} <${email}>`;
|
|
140
|
+
if (email) return email;
|
|
141
|
+
return stripWrappingQuotes(decoded);
|
|
142
|
+
}
|
|
143
|
+
function formatAddressList(input) {
|
|
144
|
+
if (!input) return "";
|
|
145
|
+
const rawItems = Array.isArray(input) ? input : splitAddressList(input);
|
|
146
|
+
return rawItems.map((item) => formatSingleAddress(item)).filter(Boolean).join(", ");
|
|
147
|
+
}
|
|
148
|
+
function formatEmailTimestamp(value) {
|
|
149
|
+
if (!value) return null;
|
|
150
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
151
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
152
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
153
|
+
month: "short",
|
|
154
|
+
day: "numeric",
|
|
155
|
+
year: "numeric",
|
|
156
|
+
hour: "numeric",
|
|
157
|
+
minute: "2-digit",
|
|
158
|
+
timeZone: "UTC"
|
|
159
|
+
}).format(date);
|
|
160
|
+
}
|
|
161
|
+
function escapeHtmlText(value) {
|
|
162
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
163
|
+
}
|
|
164
|
+
function decodeHtmlTextNodes(html) {
|
|
165
|
+
if (!HTML_ENTITY_RE.test(html) && !/\\[nrt"'\\/]|\\u/i.test(html)) return html;
|
|
166
|
+
if (typeof document !== "undefined") {
|
|
167
|
+
const template = document.createElement("template");
|
|
168
|
+
template.innerHTML = html;
|
|
169
|
+
const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_TEXT);
|
|
170
|
+
const textNodes = [];
|
|
171
|
+
let node = walker.nextNode();
|
|
172
|
+
while (node) {
|
|
173
|
+
textNodes.push(node);
|
|
174
|
+
node = walker.nextNode();
|
|
175
|
+
}
|
|
176
|
+
textNodes.forEach((textNode) => {
|
|
177
|
+
var _a;
|
|
178
|
+
textNode.nodeValue = decodeEmailDisplayText((_a = textNode.nodeValue) != null ? _a : "");
|
|
179
|
+
});
|
|
180
|
+
return template.innerHTML;
|
|
181
|
+
}
|
|
182
|
+
return html.split(/(<[^>]+>)/g).map((part) => part.startsWith("<") && part.endsWith(">") ? part : escapeHtmlText(decodeEmailDisplayText(part))).join("");
|
|
183
|
+
}
|
|
184
|
+
function decodeHtmlText(value) {
|
|
185
|
+
const withoutTags = value.replace(BR_TAG_RE, "\n").replace(/<\/(p|div|blockquote|li|tr|table|ul|ol)>/gi, "\n").replace(/<[^>]*>/g, "");
|
|
186
|
+
return decodeEmailDisplayText(withoutTags);
|
|
187
|
+
}
|
|
188
|
+
function htmlToVisibleText(html) {
|
|
189
|
+
return decodeHtmlText(html).replace(/\u00a0/g, " ").replace(/[ \t]+/g, " ").trim();
|
|
190
|
+
}
|
|
191
|
+
function serializeNode(node) {
|
|
192
|
+
const host = document.createElement("div");
|
|
193
|
+
host.appendChild(node.cloneNode(true));
|
|
194
|
+
return host.innerHTML;
|
|
195
|
+
}
|
|
196
|
+
function wrapHtmlLike(element, innerHtml) {
|
|
197
|
+
const clone = element.cloneNode(false);
|
|
198
|
+
clone.innerHTML = innerHtml;
|
|
199
|
+
return clone.outerHTML;
|
|
200
|
+
}
|
|
201
|
+
function hasDirectBr(nodes) {
|
|
202
|
+
return nodes.some((node) => node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === "br");
|
|
203
|
+
}
|
|
204
|
+
function hasDirectBlockChild(element) {
|
|
205
|
+
return Array.from(element.children).some((child) => {
|
|
206
|
+
const tagName = child.tagName.toLowerCase();
|
|
207
|
+
return tagName !== "br" && BLOCK_TAGS.has(tagName);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function makeHtmlSegment(html) {
|
|
211
|
+
const visibleText = htmlToVisibleText(html);
|
|
212
|
+
if (!html.trim() || !visibleText && !/<(?:img|hr)\b/i.test(html)) return null;
|
|
213
|
+
return { html, visibleText };
|
|
214
|
+
}
|
|
215
|
+
function splitInlineNodes(nodes, wrapper) {
|
|
216
|
+
const containsBr = hasDirectBr(nodes);
|
|
217
|
+
const chunks = [[]];
|
|
218
|
+
nodes.forEach((node) => {
|
|
219
|
+
var _a;
|
|
220
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === "br") {
|
|
221
|
+
chunks.push([]);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
(_a = chunks[chunks.length - 1]) == null ? void 0 : _a.push(serializeNode(node));
|
|
225
|
+
});
|
|
226
|
+
return chunks.map((chunk) => chunk.join("")).map((innerHtml) => {
|
|
227
|
+
if (wrapper) return wrapHtmlLike(wrapper, innerHtml);
|
|
228
|
+
return containsBr ? `<div>${innerHtml}</div>` : innerHtml;
|
|
229
|
+
}).map(makeHtmlSegment).filter((segment) => Boolean(segment));
|
|
230
|
+
}
|
|
231
|
+
function splitElementSegment(element) {
|
|
232
|
+
const tagName = element.tagName.toLowerCase();
|
|
233
|
+
if (GMAIL_SIGNATURE_RE.test(element.outerHTML) || GMAIL_QUOTE_RE.test(element.outerHTML)) {
|
|
234
|
+
const segment2 = makeHtmlSegment(element.outerHTML);
|
|
235
|
+
return segment2 ? [segment2] : [];
|
|
236
|
+
}
|
|
237
|
+
if (tagName === "div" && hasDirectBlockChild(element)) {
|
|
238
|
+
const childSegments = splitHtmlNodes(Array.from(element.childNodes));
|
|
239
|
+
return childSegments.length ? childSegments : [makeHtmlSegment(element.outerHTML)].filter(Boolean);
|
|
240
|
+
}
|
|
241
|
+
if (SPLITTABLE_BLOCK_TAGS.has(tagName) && hasDirectBr(Array.from(element.childNodes)) && !hasDirectBlockChild(element)) {
|
|
242
|
+
return splitInlineNodes(Array.from(element.childNodes), element);
|
|
243
|
+
}
|
|
244
|
+
const segment = makeHtmlSegment(element.outerHTML);
|
|
245
|
+
return segment ? [segment] : [];
|
|
246
|
+
}
|
|
247
|
+
function splitHtmlNodes(nodes) {
|
|
248
|
+
const segments = [];
|
|
249
|
+
let inlineNodes = [];
|
|
250
|
+
const flushInline = () => {
|
|
251
|
+
if (!inlineNodes.length) return;
|
|
252
|
+
segments.push(...splitInlineNodes(inlineNodes));
|
|
253
|
+
inlineNodes = [];
|
|
254
|
+
};
|
|
255
|
+
nodes.forEach((node) => {
|
|
256
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
257
|
+
const element = node;
|
|
258
|
+
const tagName = element.tagName.toLowerCase();
|
|
259
|
+
if (BLOCK_TAGS.has(tagName)) {
|
|
260
|
+
flushInline();
|
|
261
|
+
segments.push(...splitElementSegment(element));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
inlineNodes.push(node);
|
|
266
|
+
});
|
|
267
|
+
flushInline();
|
|
268
|
+
return segments;
|
|
269
|
+
}
|
|
270
|
+
function findMatchingCloseTag(html, tagName, openTagEnd) {
|
|
271
|
+
if (tagName === "hr") return openTagEnd + 1;
|
|
272
|
+
const tagPattern = new RegExp(`</?${tagName}\\b[^>]*>`, "gi");
|
|
273
|
+
tagPattern.lastIndex = openTagEnd + 1;
|
|
274
|
+
let depth = 1;
|
|
275
|
+
let match;
|
|
276
|
+
while ((match = tagPattern.exec(html)) !== null) {
|
|
277
|
+
const rawTag = match[0];
|
|
278
|
+
if (/^<\//.test(rawTag)) depth -= 1;
|
|
279
|
+
else if (!/\/\s*>$/.test(rawTag)) depth += 1;
|
|
280
|
+
if (depth === 0) return tagPattern.lastIndex;
|
|
281
|
+
}
|
|
282
|
+
return html.length;
|
|
283
|
+
}
|
|
284
|
+
function splitHtmlSegmentsFallback(html) {
|
|
285
|
+
const segments = [];
|
|
286
|
+
let cursor = 0;
|
|
287
|
+
const pushInline = (inlineHtml) => {
|
|
288
|
+
const chunks = inlineHtml.split(BR_TAG_RE);
|
|
289
|
+
const hadBr = chunks.length > 1;
|
|
290
|
+
chunks.forEach((chunk) => {
|
|
291
|
+
const segment = makeHtmlSegment(hadBr ? `<div>${chunk}</div>` : chunk);
|
|
292
|
+
if (segment) segments.push(segment);
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
while (cursor < html.length) {
|
|
296
|
+
const rest = html.slice(cursor);
|
|
297
|
+
const match = HTML_BLOCK_START_RE.exec(rest);
|
|
298
|
+
if (!match || match.index === void 0) {
|
|
299
|
+
pushInline(rest);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
if (match.index > 0) pushInline(rest.slice(0, match.index));
|
|
303
|
+
const tagStart = cursor + match.index;
|
|
304
|
+
const rawOpen = match[0];
|
|
305
|
+
const tagName = match[1].toLowerCase();
|
|
306
|
+
const openTagEnd = tagStart + rawOpen.length - 1;
|
|
307
|
+
const segmentEnd = findMatchingCloseTag(html, tagName, openTagEnd);
|
|
308
|
+
const blockHtml = html.slice(tagStart, segmentEnd);
|
|
309
|
+
if (SPLITTABLE_BLOCK_TAGS.has(tagName) && BR_TAG_RE.test(blockHtml)) {
|
|
310
|
+
const openTag = rawOpen;
|
|
311
|
+
const closeTag = `</${tagName}>`;
|
|
312
|
+
const inner = blockHtml.replace(new RegExp(`^${rawOpen.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"), "").replace(new RegExp(`${closeTag}$`, "i"), "");
|
|
313
|
+
inner.split(BR_TAG_RE).forEach((chunk) => {
|
|
314
|
+
const segment = makeHtmlSegment(`${openTag}${chunk}${closeTag}`);
|
|
315
|
+
if (segment) segments.push(segment);
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
const segment = makeHtmlSegment(blockHtml);
|
|
319
|
+
if (segment) segments.push(segment);
|
|
320
|
+
}
|
|
321
|
+
cursor = segmentEnd;
|
|
322
|
+
}
|
|
323
|
+
return segments;
|
|
324
|
+
}
|
|
325
|
+
function segmentHtmlMessage(html) {
|
|
326
|
+
if (typeof document === "undefined" || typeof Node === "undefined") {
|
|
327
|
+
return splitHtmlSegmentsFallback(html);
|
|
328
|
+
}
|
|
329
|
+
const template = document.createElement("template");
|
|
330
|
+
template.innerHTML = html;
|
|
331
|
+
return splitHtmlNodes(Array.from(template.content.childNodes));
|
|
332
|
+
}
|
|
333
|
+
function segmentTextMessage(text) {
|
|
334
|
+
var _a;
|
|
335
|
+
const normalized = decodeEmailDisplayText(text);
|
|
336
|
+
const lines = (_a = normalized.match(/[^\n]*(?:\n|$)/g)) != null ? _a : [];
|
|
337
|
+
return lines.filter((line, index) => line.length > 0 && !(index === lines.length - 1 && line === "")).map((line) => ({ text: line, visibleText: line.replace(/\u00a0/g, " ").trim() }));
|
|
338
|
+
}
|
|
339
|
+
function firstVisibleLine(text) {
|
|
340
|
+
var _a, _b;
|
|
341
|
+
return (_b = (_a = text.replace(/\u00a0/g, " ").trimStart().split(/\r?\n/).find((line) => line.trim())) == null ? void 0 : _a.trim()) != null ? _b : "";
|
|
342
|
+
}
|
|
343
|
+
function isLikelySenderNameLine(line) {
|
|
344
|
+
if (!line || line.length > 60) return false;
|
|
345
|
+
if (/[,@:;!?]|https?:\/\/|www\.|\d/.test(line)) return false;
|
|
346
|
+
const words = line.split(/\s+/).filter(Boolean);
|
|
347
|
+
if (words.length < 1 || words.length > 4) return false;
|
|
348
|
+
return words.every((word) => /^[A-Z][A-Za-z'.-]*$/.test(word));
|
|
349
|
+
}
|
|
350
|
+
function nextVisibleSegmentText(segments, fromIndex) {
|
|
351
|
+
var _a, _b;
|
|
352
|
+
for (let index = fromIndex + 1; index < segments.length; index += 1) {
|
|
353
|
+
const text = firstVisibleLine((_b = (_a = segments[index]) == null ? void 0 : _a.visibleText) != null ? _b : "");
|
|
354
|
+
if (text) return text;
|
|
355
|
+
}
|
|
356
|
+
return "";
|
|
357
|
+
}
|
|
358
|
+
function isGmailDetailsSegment(segment) {
|
|
359
|
+
return Boolean(segment.html && (GMAIL_SIGNATURE_RE.test(segment.html) || GMAIL_QUOTE_RE.test(segment.html)));
|
|
360
|
+
}
|
|
361
|
+
function isFooterBoundary(segments, index) {
|
|
362
|
+
const segment = segments[index];
|
|
363
|
+
if (!segment) return false;
|
|
364
|
+
if (isGmailDetailsSegment(segment)) return true;
|
|
365
|
+
const line = firstVisibleLine(segment.visibleText);
|
|
366
|
+
if (!line) return false;
|
|
367
|
+
if (SIGNATURE_DELIMITER_RE.test(line) || DETAILS_START_RE.test(line) || ON_WROTE_RE.test(line)) return true;
|
|
368
|
+
const nextText = nextVisibleSegmentText(segments, index);
|
|
369
|
+
if (SIGNOFF_RE.test(line)) return Boolean(nextText && (isLikelySenderNameLine(nextText) || CONTACT_DETAIL_RE.test(nextText)));
|
|
370
|
+
return isLikelySenderNameLine(line) && CONTACT_DETAIL_RE.test(nextText);
|
|
371
|
+
}
|
|
372
|
+
function splitFooterSegments(segments) {
|
|
373
|
+
const visibleIndexes = segments.map((segment, index) => segment.visibleText || isGmailDetailsSegment(segment) ? index : -1).filter((index) => index >= 0);
|
|
374
|
+
const visibleCount = visibleIndexes.length;
|
|
375
|
+
if (visibleCount < 2) return { bodySegments: segments, detailsSegments: [] };
|
|
376
|
+
const trailingCount = Math.max(Math.ceil(visibleCount * 0.4), 8);
|
|
377
|
+
const firstTrailingOrdinal = Math.max(1, visibleCount - trailingCount);
|
|
378
|
+
for (let ordinal = firstTrailingOrdinal; ordinal < visibleCount; ordinal += 1) {
|
|
379
|
+
const index = visibleIndexes[ordinal];
|
|
380
|
+
if (ordinal > 0 && isFooterBoundary(segments, index)) {
|
|
381
|
+
return { bodySegments: segments.slice(0, index), detailsSegments: segments.slice(index) };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { bodySegments: segments, detailsSegments: [] };
|
|
385
|
+
}
|
|
386
|
+
function splitEmailHtmlForDisplay(html) {
|
|
387
|
+
const sanitizedHtml = sanitizeHtml(decodeHtmlTextNodes(html));
|
|
388
|
+
const { bodySegments, detailsSegments } = splitFooterSegments(segmentHtmlMessage(sanitizedHtml));
|
|
389
|
+
if (!detailsSegments.length) return { bodyHtml: sanitizedHtml, detailsHtml: "" };
|
|
390
|
+
return {
|
|
391
|
+
bodyHtml: bodySegments.map((segment) => {
|
|
392
|
+
var _a;
|
|
393
|
+
return (_a = segment.html) != null ? _a : "";
|
|
394
|
+
}).join(""),
|
|
395
|
+
detailsHtml: detailsSegments.map((segment) => {
|
|
396
|
+
var _a;
|
|
397
|
+
return (_a = segment.html) != null ? _a : "";
|
|
398
|
+
}).join("")
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function splitEmailTextForDisplay(text) {
|
|
402
|
+
const decodedText = decodeEmailDisplayText(text);
|
|
403
|
+
const { bodySegments, detailsSegments } = splitFooterSegments(segmentTextMessage(decodedText));
|
|
404
|
+
if (!detailsSegments.length) return { bodyText: decodedText, detailsText: "" };
|
|
405
|
+
return {
|
|
406
|
+
bodyText: bodySegments.map((segment) => {
|
|
407
|
+
var _a;
|
|
408
|
+
return (_a = segment.text) != null ? _a : "";
|
|
409
|
+
}).join(""),
|
|
410
|
+
detailsText: detailsSegments.map((segment) => {
|
|
411
|
+
var _a;
|
|
412
|
+
return (_a = segment.text) != null ? _a : "";
|
|
413
|
+
}).join("")
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function emailBodySnippet(input, maxLength = 140) {
|
|
417
|
+
var _a, _b, _c, _d;
|
|
418
|
+
const html = (_a = input.bodyHtml) == null ? void 0 : _a.trim();
|
|
419
|
+
if (html) {
|
|
420
|
+
return decodeEmailDisplayText(htmlToTextSnippet(splitEmailHtmlForDisplay(html).bodyHtml, maxLength)).trim();
|
|
421
|
+
}
|
|
422
|
+
const text = (_b = input.body) != null ? _b : "";
|
|
423
|
+
const bodyText = splitEmailTextForDisplay(text).bodyText;
|
|
424
|
+
const firstLine = (_d = (_c = bodyText.split("\n").find((line) => line.trim())) == null ? void 0 : _c.trim()) != null ? _d : "";
|
|
425
|
+
return decodeEmailDisplayText(firstLine).replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
426
|
+
}
|
|
427
|
+
export {
|
|
428
|
+
decodeEmailDisplayText,
|
|
429
|
+
emailBodySnippet,
|
|
430
|
+
formatAddressList,
|
|
431
|
+
formatEmailTimestamp,
|
|
432
|
+
normalizeEmailSender,
|
|
433
|
+
splitEmailHtmlForDisplay,
|
|
434
|
+
splitEmailTextForDisplay
|
|
435
|
+
};
|
|
436
|
+
//# sourceMappingURL=email-display-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/email-display-helpers.ts"],"sourcesContent":["import { htmlToTextSnippet, sanitizeHtml } from \"../internal/safe-html\"\n\nexport interface NormalizedEmailSender {\n name: string\n email: string | null\n}\n\nexport interface EmailDisplaySenderInput {\n name?: string | null\n email?: string | null\n fallbackName?: string\n}\n\nexport interface SplitEmailHtmlResult {\n bodyHtml: string\n detailsHtml: string\n}\n\nexport interface SplitEmailTextResult {\n bodyText: string\n detailsText: string\n}\n\nconst HTML_ENTITY_RE = /&(?:#x[0-9a-f]+|#\\d+|[a-z][a-z0-9]+);?/i\nconst EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i\nconst ANGLE_ADDRESS_RE = /^\\s*(.*?)\\s*<\\s*([^<>\\s]+@[^<>\\s]+)\\s*>\\s*$/\nconst BR_TAG_RE = /<br\\s*\\/?>/gi\n\nconst SPLITTABLE_BLOCK_TAGS = new Set([\"p\", \"div\"])\nconst WHOLE_BLOCK_TAGS = new Set([\"blockquote\", \"table\", \"ul\", \"ol\", \"hr\"])\nconst BLOCK_TAGS = new Set([...SPLITTABLE_BLOCK_TAGS, ...WHOLE_BLOCK_TAGS])\nconst HTML_BLOCK_START_RE = /<(p|div|blockquote|table|ul|ol|hr)\\b[^>]*>/i\n\nconst SIGNATURE_DELIMITER_RE = /^--\\s*$/\nconst GMAIL_SIGNATURE_RE = /\\b(?:gmail_signature|gmail_signature_prefix|gmail_extra)\\b/i\nconst GMAIL_QUOTE_RE = /<blockquote\\b[^>]*\\bclass=[\"'][^\"']*\\bgmail_quote\\b/i\nconst ON_WROTE_RE = /^On\\s.+wrote:\\s*$/i\nconst SIGNOFF_RE = /^(?:thanks,|thank you,|best,|regards,|sincerely,)$/i\nconst DETAILS_START_RE = /^(?:confidentiality notice\\b|this message and any attachments\\b|this email and any attachments\\b|the information contained in this message\\b|this communication may contain\\b|unsubscribe\\b|manage your preferences\\b)/i\nconst CONTACT_DETAIL_RE = /(?:@|https?:\\/\\/|www\\.|\\+?\\d[\\d\\s().-]{6,}\\d|\\b(?:ceo|cfo|cto|coo|founder|co-founder|director|manager|vp|vice president|president|head of|sales|marketing|operations|account|customer success|success|support|engineer|consultant|partner|principal|advisor|associate)\\b|\\b(?:inc|llc|ltd|corp|corporation|company|co\\.)\\b)/i\n\nfunction safeCodePoint(value: number): string {\n return Number.isInteger(value) && value >= 0 && value <= 0x10ffff ? String.fromCodePoint(value) : \"\"\n}\n\nfunction decodeHtmlEntities(value: string): string {\n const namedEntities: Record<string, string> = {\n amp: \"&\",\n apos: \"'\",\n colon: \":\",\n gt: \">\",\n lt: \"<\",\n nbsp: \" \",\n newline: \"\\n\",\n quot: '\"',\n tab: \"\\t\",\n }\n\n let decoded = value\n for (let i = 0; i < 4; i += 1) {\n const next = decoded\n .replace(/&#x([0-9a-f]+);?/gi, (_match, hex: string) => safeCodePoint(Number.parseInt(hex, 16)))\n .replace(/&#(\\d+);?/g, (_match, decimal: string) => safeCodePoint(Number.parseInt(decimal, 10)))\n .replace(/&([a-z][a-z0-9]+);?/gi, (match, name: string) => namedEntities[name.toLowerCase()] ?? match)\n\n if (next === decoded) return decoded\n decoded = next\n }\n\n return decoded\n}\n\nfunction decodeJsonEscapes(value: string): string {\n return value\n .replace(/\\\\u\\{([0-9a-f]{1,6})\\}/gi, (_match, hex: string) => safeCodePoint(Number.parseInt(hex, 16)))\n .replace(/\\\\u([0-9a-f]{4})/gi, (_match, hex: string) => safeCodePoint(Number.parseInt(hex, 16)))\n .replace(/\\\\r\\\\n|\\\\n|\\\\r/g, \"\\n\")\n .replace(/\\\\t/g, \"\\t\")\n .replace(/\\\\([\"'\\\\/])/g, \"$1\")\n}\n\nfunction maybeParseJsonString(value: string): string {\n const trimmed = value.trim()\n if (!trimmed.startsWith('\"') || !trimmed.endsWith('\"')) return value\n if (!/[\\\\&]/.test(trimmed)) return value\n\n try {\n const parsed = JSON.parse(trimmed) as unknown\n return typeof parsed === \"string\" ? parsed : value\n } catch {\n return value\n }\n}\n\n/**\n * Decodes display-only email text so UI labels, body fallbacks, collapsed rows,\n * and snippets do not show HTML entities or JSON escape artifacts. It does not\n * sanitize HTML; sanitize at the HTML render boundary before using markup.\n */\nexport function decodeEmailDisplayText(value: string): string {\n let decoded = maybeParseJsonString(value).replace(/\\r\\n?/g, \"\\n\")\n\n for (let i = 0; i < 4; i += 1) {\n const next = decodeHtmlEntities(decodeJsonEscapes(decoded))\n if (next === decoded) break\n decoded = next\n }\n\n return decoded.replace(/\\u00a0/g, \" \")\n}\n\nfunction stripWrappingQuotes(value: string): string {\n const trimmed = value.trim()\n if ((trimmed.startsWith('\"') && trimmed.endsWith('\"')) || (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\"))) {\n return trimmed.slice(1, -1).trim()\n }\n return trimmed\n}\n\nfunction extractEmail(value: string): string | null {\n const decoded = decodeEmailDisplayText(value)\n const angleMatch = decoded.match(ANGLE_ADDRESS_RE)\n const email = angleMatch?.[2] ?? decoded.match(EMAIL_RE)?.[0]\n return email ? email.trim() : null\n}\n\nfunction extractNameFromAddress(value: string): string {\n const decoded = decodeEmailDisplayText(value)\n const angleMatch = decoded.match(ANGLE_ADDRESS_RE)\n if (angleMatch) return stripWrappingQuotes(angleMatch[1] ?? \"\")\n return stripWrappingQuotes(decoded.replace(EMAIL_RE, \"\").replace(/[<>]/g, \"\").trim())\n}\n\nfunction cleanDisplayName(value: string, email: string | null): string {\n let name = stripWrappingQuotes(decodeEmailDisplayText(value))\n if (email) {\n name = name\n .replace(new RegExp(`<\\\\s*${email.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}\\\\s*>`, \"i\"), \"\")\n .replace(new RegExp(`^${email.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}$`, \"i\"), \"\")\n .trim()\n }\n return stripWrappingQuotes(name)\n}\n\nexport function normalizeEmailSender(input: EmailDisplaySenderInput): NormalizedEmailSender {\n const fallbackName = decodeEmailDisplayText(input.fallbackName ?? \"Unknown sender\").trim() || \"Unknown sender\"\n const rawName = input.name ? decodeEmailDisplayText(input.name) : \"\"\n const rawEmail = input.email ? decodeEmailDisplayText(input.email) : \"\"\n const email = extractEmail(rawEmail) ?? extractEmail(rawName)\n\n const nameFromName = rawName ? cleanDisplayName(extractNameFromAddress(rawName) || rawName, email) : \"\"\n const nameFromEmail = rawEmail ? cleanDisplayName(extractNameFromAddress(rawEmail), email) : \"\"\n const name = nameFromName || nameFromEmail || email || fallbackName\n\n return { name, email }\n}\n\nfunction splitAddressList(value: string): string[] {\n const parts: string[] = []\n let current = \"\"\n let quote: '\"' | \"'\" | null = null\n let angleDepth = 0\n\n for (let index = 0; index < value.length; index += 1) {\n const char = value[index]\n\n if (quote) {\n current += char\n if (char === quote && value[index - 1] !== \"\\\\\") quote = null\n continue\n }\n\n if (char === '\"' || char === \"'\") {\n quote = char\n current += char\n continue\n }\n\n if (char === \"<\") angleDepth += 1\n if (char === \">\" && angleDepth > 0) angleDepth -= 1\n\n if (char === \",\" && angleDepth === 0) {\n if (current.trim()) parts.push(current.trim())\n current = \"\"\n continue\n }\n\n current += char\n }\n\n if (current.trim()) parts.push(current.trim())\n return parts\n}\n\nfunction formatSingleAddress(value: string): string {\n const decoded = decodeEmailDisplayText(value).trim()\n if (!decoded) return \"\"\n\n const email = extractEmail(decoded)\n const name = cleanDisplayName(extractNameFromAddress(decoded), email)\n\n if (email && name) return `${name} <${email}>`\n if (email) return email\n return stripWrappingQuotes(decoded)\n}\n\nexport function formatAddressList(input?: string | string[] | null): string {\n if (!input) return \"\"\n\n const rawItems = Array.isArray(input) ? input : splitAddressList(input)\n return rawItems\n .map((item) => formatSingleAddress(item))\n .filter(Boolean)\n .join(\", \")\n}\n\nexport function formatEmailTimestamp(value?: string | Date | null): string | null {\n if (!value) return null\n const date = value instanceof Date ? value : new Date(value)\n if (Number.isNaN(date.getTime())) return null\n\n return new Intl.DateTimeFormat(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n timeZone: \"UTC\",\n }).format(date)\n}\n\ntype MessageSegment = { html?: string; text?: string; visibleText: string }\n\nfunction escapeHtmlText(value: string): string {\n return value.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")\n}\n\nfunction decodeHtmlTextNodes(html: string): string {\n if (!HTML_ENTITY_RE.test(html) && !/\\\\[nrt\"'\\\\/]|\\\\u/i.test(html)) return html\n\n if (typeof document !== \"undefined\") {\n const template = document.createElement(\"template\")\n template.innerHTML = html\n const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_TEXT)\n const textNodes: Text[] = []\n let node = walker.nextNode()\n while (node) {\n textNodes.push(node as Text)\n node = walker.nextNode()\n }\n textNodes.forEach((textNode) => {\n textNode.nodeValue = decodeEmailDisplayText(textNode.nodeValue ?? \"\")\n })\n return template.innerHTML\n }\n\n return html\n .split(/(<[^>]+>)/g)\n .map((part) => (part.startsWith(\"<\") && part.endsWith(\">\") ? part : escapeHtmlText(decodeEmailDisplayText(part))))\n .join(\"\")\n}\n\nfunction decodeHtmlText(value: string): string {\n const withoutTags = value\n .replace(BR_TAG_RE, \"\\n\")\n .replace(/<\\/(p|div|blockquote|li|tr|table|ul|ol)>/gi, \"\\n\")\n .replace(/<[^>]*>/g, \"\")\n\n return decodeEmailDisplayText(withoutTags)\n}\n\nfunction htmlToVisibleText(html: string): string {\n return decodeHtmlText(html).replace(/\\u00a0/g, \" \").replace(/[ \\t]+/g, \" \").trim()\n}\n\nfunction serializeNode(node: Node): string {\n const host = document.createElement(\"div\")\n host.appendChild(node.cloneNode(true))\n return host.innerHTML\n}\n\nfunction wrapHtmlLike(element: Element, innerHtml: string): string {\n const clone = element.cloneNode(false) as HTMLElement\n clone.innerHTML = innerHtml\n return clone.outerHTML\n}\n\nfunction hasDirectBr(nodes: readonly Node[]): boolean {\n return nodes.some((node) => node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName.toLowerCase() === \"br\")\n}\n\nfunction hasDirectBlockChild(element: Element): boolean {\n return Array.from(element.children).some((child) => {\n const tagName = child.tagName.toLowerCase()\n return tagName !== \"br\" && BLOCK_TAGS.has(tagName)\n })\n}\n\nfunction makeHtmlSegment(html: string): MessageSegment | null {\n const visibleText = htmlToVisibleText(html)\n if (!html.trim() || (!visibleText && !/<(?:img|hr)\\b/i.test(html))) return null\n return { html, visibleText }\n}\n\nfunction splitInlineNodes(nodes: readonly Node[], wrapper?: Element): MessageSegment[] {\n const containsBr = hasDirectBr(nodes)\n const chunks: string[][] = [[]]\n\n nodes.forEach((node) => {\n if (node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName.toLowerCase() === \"br\") {\n chunks.push([])\n return\n }\n\n chunks[chunks.length - 1]?.push(serializeNode(node))\n })\n\n return chunks\n .map((chunk) => chunk.join(\"\"))\n .map((innerHtml) => {\n if (wrapper) return wrapHtmlLike(wrapper, innerHtml)\n return containsBr ? `<div>${innerHtml}</div>` : innerHtml\n })\n .map(makeHtmlSegment)\n .filter((segment): segment is MessageSegment => Boolean(segment))\n}\n\nfunction splitElementSegment(element: Element): MessageSegment[] {\n const tagName = element.tagName.toLowerCase()\n\n if (GMAIL_SIGNATURE_RE.test(element.outerHTML) || GMAIL_QUOTE_RE.test(element.outerHTML)) {\n const segment = makeHtmlSegment(element.outerHTML)\n return segment ? [segment] : []\n }\n\n if (tagName === \"div\" && hasDirectBlockChild(element)) {\n const childSegments = splitHtmlNodes(Array.from(element.childNodes))\n return childSegments.length ? childSegments : ([makeHtmlSegment(element.outerHTML)].filter(Boolean) as MessageSegment[])\n }\n\n if (SPLITTABLE_BLOCK_TAGS.has(tagName) && hasDirectBr(Array.from(element.childNodes)) && !hasDirectBlockChild(element)) {\n return splitInlineNodes(Array.from(element.childNodes), element)\n }\n\n const segment = makeHtmlSegment(element.outerHTML)\n return segment ? [segment] : []\n}\n\nfunction splitHtmlNodes(nodes: readonly Node[]): MessageSegment[] {\n const segments: MessageSegment[] = []\n let inlineNodes: Node[] = []\n\n const flushInline = () => {\n if (!inlineNodes.length) return\n segments.push(...splitInlineNodes(inlineNodes))\n inlineNodes = []\n }\n\n nodes.forEach((node) => {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as Element\n const tagName = element.tagName.toLowerCase()\n if (BLOCK_TAGS.has(tagName)) {\n flushInline()\n segments.push(...splitElementSegment(element))\n return\n }\n }\n\n inlineNodes.push(node)\n })\n\n flushInline()\n return segments\n}\n\nfunction findMatchingCloseTag(html: string, tagName: string, openTagEnd: number): number {\n if (tagName === \"hr\") return openTagEnd + 1\n\n const tagPattern = new RegExp(`</?${tagName}\\\\b[^>]*>`, \"gi\")\n tagPattern.lastIndex = openTagEnd + 1\n let depth = 1\n let match: RegExpExecArray | null\n\n while ((match = tagPattern.exec(html)) !== null) {\n const rawTag = match[0]\n if (/^<\\//.test(rawTag)) depth -= 1\n else if (!/\\/\\s*>$/.test(rawTag)) depth += 1\n if (depth === 0) return tagPattern.lastIndex\n }\n\n return html.length\n}\n\nfunction splitHtmlSegmentsFallback(html: string): MessageSegment[] {\n const segments: MessageSegment[] = []\n let cursor = 0\n\n const pushInline = (inlineHtml: string) => {\n const chunks = inlineHtml.split(BR_TAG_RE)\n const hadBr = chunks.length > 1\n chunks.forEach((chunk) => {\n const segment = makeHtmlSegment(hadBr ? `<div>${chunk}</div>` : chunk)\n if (segment) segments.push(segment)\n })\n }\n\n while (cursor < html.length) {\n const rest = html.slice(cursor)\n const match = HTML_BLOCK_START_RE.exec(rest)\n if (!match || match.index === undefined) {\n pushInline(rest)\n break\n }\n\n if (match.index > 0) pushInline(rest.slice(0, match.index))\n\n const tagStart = cursor + match.index\n const rawOpen = match[0]\n const tagName = match[1].toLowerCase()\n const openTagEnd = tagStart + rawOpen.length - 1\n const segmentEnd = findMatchingCloseTag(html, tagName, openTagEnd)\n const blockHtml = html.slice(tagStart, segmentEnd)\n\n if (SPLITTABLE_BLOCK_TAGS.has(tagName) && BR_TAG_RE.test(blockHtml)) {\n const openTag = rawOpen\n const closeTag = `</${tagName}>`\n const inner = blockHtml.replace(new RegExp(`^${rawOpen.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}`, \"i\"), \"\").replace(new RegExp(`${closeTag}$`, \"i\"), \"\")\n inner.split(BR_TAG_RE).forEach((chunk) => {\n const segment = makeHtmlSegment(`${openTag}${chunk}${closeTag}`)\n if (segment) segments.push(segment)\n })\n } else {\n const segment = makeHtmlSegment(blockHtml)\n if (segment) segments.push(segment)\n }\n\n cursor = segmentEnd\n }\n\n return segments\n}\n\nfunction segmentHtmlMessage(html: string): MessageSegment[] {\n if (typeof document === \"undefined\" || typeof Node === \"undefined\") {\n return splitHtmlSegmentsFallback(html)\n }\n\n const template = document.createElement(\"template\")\n template.innerHTML = html\n return splitHtmlNodes(Array.from(template.content.childNodes))\n}\n\nfunction segmentTextMessage(text: string): MessageSegment[] {\n const normalized = decodeEmailDisplayText(text)\n const lines: string[] = normalized.match(/[^\\n]*(?:\\n|$)/g) ?? []\n return lines\n .filter((line, index) => line.length > 0 && !(index === lines.length - 1 && line === \"\"))\n .map((line) => ({ text: line, visibleText: line.replace(/\\u00a0/g, \" \").trim() }))\n}\n\nfunction firstVisibleLine(text: string): string {\n return text.replace(/\\u00a0/g, \" \").trimStart().split(/\\r?\\n/).find((line) => line.trim())?.trim() ?? \"\"\n}\n\nfunction isLikelySenderNameLine(line: string): boolean {\n if (!line || line.length > 60) return false\n if (/[,@:;!?]|https?:\\/\\/|www\\.|\\d/.test(line)) return false\n\n const words = line.split(/\\s+/).filter(Boolean)\n if (words.length < 1 || words.length > 4) return false\n\n return words.every((word) => /^[A-Z][A-Za-z'.-]*$/.test(word))\n}\n\nfunction nextVisibleSegmentText(segments: MessageSegment[], fromIndex: number): string {\n for (let index = fromIndex + 1; index < segments.length; index += 1) {\n const text = firstVisibleLine(segments[index]?.visibleText ?? \"\")\n if (text) return text\n }\n return \"\"\n}\n\nfunction isGmailDetailsSegment(segment: MessageSegment): boolean {\n return Boolean(segment.html && (GMAIL_SIGNATURE_RE.test(segment.html) || GMAIL_QUOTE_RE.test(segment.html)))\n}\n\nfunction isFooterBoundary(segments: MessageSegment[], index: number): boolean {\n const segment = segments[index]\n if (!segment) return false\n if (isGmailDetailsSegment(segment)) return true\n\n const line = firstVisibleLine(segment.visibleText)\n if (!line) return false\n if (SIGNATURE_DELIMITER_RE.test(line) || DETAILS_START_RE.test(line) || ON_WROTE_RE.test(line)) return true\n\n const nextText = nextVisibleSegmentText(segments, index)\n if (SIGNOFF_RE.test(line)) return Boolean(nextText && (isLikelySenderNameLine(nextText) || CONTACT_DETAIL_RE.test(nextText)))\n\n return isLikelySenderNameLine(line) && CONTACT_DETAIL_RE.test(nextText)\n}\n\nfunction splitFooterSegments(segments: MessageSegment[]): { bodySegments: MessageSegment[]; detailsSegments: MessageSegment[] } {\n const visibleIndexes = segments\n .map((segment, index) => (segment.visibleText || isGmailDetailsSegment(segment) ? index : -1))\n .filter((index) => index >= 0)\n const visibleCount = visibleIndexes.length\n if (visibleCount < 2) return { bodySegments: segments, detailsSegments: [] }\n\n const trailingCount = Math.max(Math.ceil(visibleCount * 0.4), 8)\n const firstTrailingOrdinal = Math.max(1, visibleCount - trailingCount)\n\n for (let ordinal = firstTrailingOrdinal; ordinal < visibleCount; ordinal += 1) {\n const index = visibleIndexes[ordinal]\n if (ordinal > 0 && isFooterBoundary(segments, index)) {\n return { bodySegments: segments.slice(0, index), detailsSegments: segments.slice(index) }\n }\n }\n\n return { bodySegments: segments, detailsSegments: [] }\n}\n\nexport function splitEmailHtmlForDisplay(html: string): SplitEmailHtmlResult {\n const sanitizedHtml = sanitizeHtml(decodeHtmlTextNodes(html))\n const { bodySegments, detailsSegments } = splitFooterSegments(segmentHtmlMessage(sanitizedHtml))\n\n if (!detailsSegments.length) return { bodyHtml: sanitizedHtml, detailsHtml: \"\" }\n\n return {\n bodyHtml: bodySegments.map((segment) => segment.html ?? \"\").join(\"\"),\n detailsHtml: detailsSegments.map((segment) => segment.html ?? \"\").join(\"\"),\n }\n}\n\nexport function splitEmailTextForDisplay(text: string): SplitEmailTextResult {\n const decodedText = decodeEmailDisplayText(text)\n const { bodySegments, detailsSegments } = splitFooterSegments(segmentTextMessage(decodedText))\n\n if (!detailsSegments.length) return { bodyText: decodedText, detailsText: \"\" }\n\n return {\n bodyText: bodySegments.map((segment) => segment.text ?? \"\").join(\"\"),\n detailsText: detailsSegments.map((segment) => segment.text ?? \"\").join(\"\"),\n }\n}\n\nexport function emailBodySnippet(input: { bodyHtml?: string | null; body?: string | null }, maxLength = 140): string {\n const html = input.bodyHtml?.trim()\n if (html) {\n return decodeEmailDisplayText(htmlToTextSnippet(splitEmailHtmlForDisplay(html).bodyHtml, maxLength)).trim()\n }\n\n const text = input.body ?? \"\"\n const bodyText = splitEmailTextForDisplay(text).bodyText\n const firstLine = bodyText.split(\"\\n\").find((line) => line.trim())?.trim() ?? \"\"\n return decodeEmailDisplayText(firstLine).replace(/\\s+/g, \" \").trim().slice(0, maxLength)\n}\n"],"mappings":"AAAA,SAAS,mBAAmB,oBAAoB;AAuBhD,MAAM,iBAAiB;AACvB,MAAM,WAAW;AACjB,MAAM,mBAAmB;AACzB,MAAM,YAAY;AAElB,MAAM,wBAAwB,oBAAI,IAAI,CAAC,KAAK,KAAK,CAAC;AAClD,MAAM,mBAAmB,oBAAI,IAAI,CAAC,cAAc,SAAS,MAAM,MAAM,IAAI,CAAC;AAC1E,MAAM,aAAa,oBAAI,IAAI,CAAC,GAAG,uBAAuB,GAAG,gBAAgB,CAAC;AAC1E,MAAM,sBAAsB;AAE5B,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;AAC3B,MAAM,iBAAiB;AACvB,MAAM,cAAc;AACpB,MAAM,aAAa;AACnB,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;AAE1B,SAAS,cAAc,OAAuB;AAC5C,SAAO,OAAO,UAAU,KAAK,KAAK,SAAS,KAAK,SAAS,UAAW,OAAO,cAAc,KAAK,IAAI;AACpG;AAEA,SAAS,mBAAmB,OAAuB;AACjD,QAAM,gBAAwC;AAAA,IAC5C,KAAK;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA,EACP;AAEA,MAAI,UAAU;AACd,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;AAC7B,UAAM,OAAO,QACV,QAAQ,sBAAsB,CAAC,QAAQ,QAAgB,cAAc,OAAO,SAAS,KAAK,EAAE,CAAC,CAAC,EAC9F,QAAQ,cAAc,CAAC,QAAQ,YAAoB,cAAc,OAAO,SAAS,SAAS,EAAE,CAAC,CAAC,EAC9F,QAAQ,yBAAyB,CAAC,OAAO,SAAc;AA/D9D;AA+DiE,iCAAc,KAAK,YAAY,CAAC,MAAhC,YAAqC;AAAA,KAAK;AAEvG,QAAI,SAAS,QAAS,QAAO;AAC7B,cAAU;AAAA,EACZ;AAEA,SAAO;AACT;AAEA,SAAS,kBAAkB,OAAuB;AAChD,SAAO,MACJ,QAAQ,4BAA4B,CAAC,QAAQ,QAAgB,cAAc,OAAO,SAAS,KAAK,EAAE,CAAC,CAAC,EACpG,QAAQ,sBAAsB,CAAC,QAAQ,QAAgB,cAAc,OAAO,SAAS,KAAK,EAAE,CAAC,CAAC,EAC9F,QAAQ,mBAAmB,IAAI,EAC/B,QAAQ,QAAQ,GAAI,EACpB,QAAQ,gBAAgB,IAAI;AACjC;AAEA,SAAS,qBAAqB,OAAuB;AACnD,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAQ,WAAW,GAAG,KAAK,CAAC,QAAQ,SAAS,GAAG,EAAG,QAAO;AAC/D,MAAI,CAAC,QAAQ,KAAK,OAAO,EAAG,QAAO;AAEnC,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,WAAO,OAAO,WAAW,WAAW,SAAS;AAAA,EAC/C,SAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,SAAS,uBAAuB,OAAuB;AAC5D,MAAI,UAAU,qBAAqB,KAAK,EAAE,QAAQ,UAAU,IAAI;AAEhE,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;AAC7B,UAAM,OAAO,mBAAmB,kBAAkB,OAAO,CAAC;AAC1D,QAAI,SAAS,QAAS;AACtB,cAAU;AAAA,EACZ;AAEA,SAAO,QAAQ,QAAQ,WAAW,GAAG;AACvC;AAEA,SAAS,oBAAoB,OAAuB;AAClD,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAK,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,KAAO,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,GAAI;AAC5G,WAAO,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK;AAAA,EACnC;AACA,SAAO;AACT;AAEA,SAAS,aAAa,OAA8B;AAvHpD;AAwHE,QAAM,UAAU,uBAAuB,KAAK;AAC5C,QAAM,aAAa,QAAQ,MAAM,gBAAgB;AACjD,QAAM,SAAQ,8CAAa,OAAb,aAAmB,aAAQ,MAAM,QAAQ,MAAtB,mBAA0B;AAC3D,SAAO,QAAQ,MAAM,KAAK,IAAI;AAChC;AAEA,SAAS,uBAAuB,OAAuB;AA9HvD;AA+HE,QAAM,UAAU,uBAAuB,KAAK;AAC5C,QAAM,aAAa,QAAQ,MAAM,gBAAgB;AACjD,MAAI,WAAY,QAAO,qBAAoB,gBAAW,CAAC,MAAZ,YAAiB,EAAE;AAC9D,SAAO,oBAAoB,QAAQ,QAAQ,UAAU,EAAE,EAAE,QAAQ,SAAS,EAAE,EAAE,KAAK,CAAC;AACtF;AAEA,SAAS,iBAAiB,OAAe,OAA8B;AACrE,MAAI,OAAO,oBAAoB,uBAAuB,KAAK,CAAC;AAC5D,MAAI,OAAO;AACT,WAAO,KACJ,QAAQ,IAAI,OAAO,QAAQ,MAAM,QAAQ,uBAAuB,MAAM,CAAC,SAAS,GAAG,GAAG,EAAE,EACxF,QAAQ,IAAI,OAAO,IAAI,MAAM,QAAQ,uBAAuB,MAAM,CAAC,KAAK,GAAG,GAAG,EAAE,EAChF,KAAK;AAAA,EACV;AACA,SAAO,oBAAoB,IAAI;AACjC;AAEO,SAAS,qBAAqB,OAAuD;AAhJ5F;AAiJE,QAAM,eAAe,wBAAuB,WAAM,iBAAN,YAAsB,gBAAgB,EAAE,KAAK,KAAK;AAC9F,QAAM,UAAU,MAAM,OAAO,uBAAuB,MAAM,IAAI,IAAI;AAClE,QAAM,WAAW,MAAM,QAAQ,uBAAuB,MAAM,KAAK,IAAI;AACrE,QAAM,SAAQ,kBAAa,QAAQ,MAArB,YAA0B,aAAa,OAAO;AAE5D,QAAM,eAAe,UAAU,iBAAiB,uBAAuB,OAAO,KAAK,SAAS,KAAK,IAAI;AACrG,QAAM,gBAAgB,WAAW,iBAAiB,uBAAuB,QAAQ,GAAG,KAAK,IAAI;AAC7F,QAAM,OAAO,gBAAgB,iBAAiB,SAAS;AAEvD,SAAO,EAAE,MAAM,MAAM;AACvB;AAEA,SAAS,iBAAiB,OAAyB;AACjD,QAAM,QAAkB,CAAC;AACzB,MAAI,UAAU;AACd,MAAI,QAA0B;AAC9B,MAAI,aAAa;AAEjB,WAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,GAAG;AACpD,UAAM,OAAO,MAAM,KAAK;AAExB,QAAI,OAAO;AACT,iBAAW;AACX,UAAI,SAAS,SAAS,MAAM,QAAQ,CAAC,MAAM,KAAM,SAAQ;AACzD;AAAA,IACF;AAEA,QAAI,SAAS,OAAO,SAAS,KAAK;AAChC,cAAQ;AACR,iBAAW;AACX;AAAA,IACF;AAEA,QAAI,SAAS,IAAK,eAAc;AAChC,QAAI,SAAS,OAAO,aAAa,EAAG,eAAc;AAElD,QAAI,SAAS,OAAO,eAAe,GAAG;AACpC,UAAI,QAAQ,KAAK,EAAG,OAAM,KAAK,QAAQ,KAAK,CAAC;AAC7C,gBAAU;AACV;AAAA,IACF;AAEA,eAAW;AAAA,EACb;AAEA,MAAI,QAAQ,KAAK,EAAG,OAAM,KAAK,QAAQ,KAAK,CAAC;AAC7C,SAAO;AACT;AAEA,SAAS,oBAAoB,OAAuB;AAClD,QAAM,UAAU,uBAAuB,KAAK,EAAE,KAAK;AACnD,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,QAAQ,aAAa,OAAO;AAClC,QAAM,OAAO,iBAAiB,uBAAuB,OAAO,GAAG,KAAK;AAEpE,MAAI,SAAS,KAAM,QAAO,GAAG,IAAI,KAAK,KAAK;AAC3C,MAAI,MAAO,QAAO;AAClB,SAAO,oBAAoB,OAAO;AACpC;AAEO,SAAS,kBAAkB,OAA0C;AAC1E,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,WAAW,MAAM,QAAQ,KAAK,IAAI,QAAQ,iBAAiB,KAAK;AACtE,SAAO,SACJ,IAAI,CAAC,SAAS,oBAAoB,IAAI,CAAC,EACvC,OAAO,OAAO,EACd,KAAK,IAAI;AACd;AAEO,SAAS,qBAAqB,OAA6C;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;AAC3D,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO;AAEzC,SAAO,IAAI,KAAK,eAAe,SAAS;AAAA,IACtC,OAAO;AAAA,IACP,KAAK;AAAA,IACL,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ,CAAC,EAAE,OAAO,IAAI;AAChB;AAIA,SAAS,eAAe,OAAuB;AAC7C,SAAO,MAAM,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM;AAChF;AAEA,SAAS,oBAAoB,MAAsB;AACjD,MAAI,CAAC,eAAe,KAAK,IAAI,KAAK,CAAC,oBAAoB,KAAK,IAAI,EAAG,QAAO;AAE1E,MAAI,OAAO,aAAa,aAAa;AACnC,UAAM,WAAW,SAAS,cAAc,UAAU;AAClD,aAAS,YAAY;AACrB,UAAM,SAAS,SAAS,iBAAiB,SAAS,SAAS,WAAW,SAAS;AAC/E,UAAM,YAAoB,CAAC;AAC3B,QAAI,OAAO,OAAO,SAAS;AAC3B,WAAO,MAAM;AACX,gBAAU,KAAK,IAAY;AAC3B,aAAO,OAAO,SAAS;AAAA,IACzB;AACA,cAAU,QAAQ,CAAC,aAAa;AA1PpC;AA2PM,eAAS,YAAY,wBAAuB,cAAS,cAAT,YAAsB,EAAE;AAAA,IACtE,CAAC;AACD,WAAO,SAAS;AAAA,EAClB;AAEA,SAAO,KACJ,MAAM,YAAY,EAClB,IAAI,CAAC,SAAU,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,IAAI,OAAO,eAAe,uBAAuB,IAAI,CAAC,CAAE,EAChH,KAAK,EAAE;AACZ;AAEA,SAAS,eAAe,OAAuB;AAC7C,QAAM,cAAc,MACjB,QAAQ,WAAW,IAAI,EACvB,QAAQ,8CAA8C,IAAI,EAC1D,QAAQ,YAAY,EAAE;AAEzB,SAAO,uBAAuB,WAAW;AAC3C;AAEA,SAAS,kBAAkB,MAAsB;AAC/C,SAAO,eAAe,IAAI,EAAE,QAAQ,WAAW,GAAG,EAAE,QAAQ,WAAW,GAAG,EAAE,KAAK;AACnF;AAEA,SAAS,cAAc,MAAoB;AACzC,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,YAAY,KAAK,UAAU,IAAI,CAAC;AACrC,SAAO,KAAK;AACd;AAEA,SAAS,aAAa,SAAkB,WAA2B;AACjE,QAAM,QAAQ,QAAQ,UAAU,KAAK;AACrC,QAAM,YAAY;AAClB,SAAO,MAAM;AACf;AAEA,SAAS,YAAY,OAAiC;AACpD,SAAO,MAAM,KAAK,CAAC,SAAS,KAAK,aAAa,KAAK,gBAAiB,KAAiB,QAAQ,YAAY,MAAM,IAAI;AACrH;AAEA,SAAS,oBAAoB,SAA2B;AACtD,SAAO,MAAM,KAAK,QAAQ,QAAQ,EAAE,KAAK,CAAC,UAAU;AAClD,UAAM,UAAU,MAAM,QAAQ,YAAY;AAC1C,WAAO,YAAY,QAAQ,WAAW,IAAI,OAAO;AAAA,EACnD,CAAC;AACH;AAEA,SAAS,gBAAgB,MAAqC;AAC5D,QAAM,cAAc,kBAAkB,IAAI;AAC1C,MAAI,CAAC,KAAK,KAAK,KAAM,CAAC,eAAe,CAAC,iBAAiB,KAAK,IAAI,EAAI,QAAO;AAC3E,SAAO,EAAE,MAAM,YAAY;AAC7B;AAEA,SAAS,iBAAiB,OAAwB,SAAqC;AACrF,QAAM,aAAa,YAAY,KAAK;AACpC,QAAM,SAAqB,CAAC,CAAC,CAAC;AAE9B,QAAM,QAAQ,CAAC,SAAS;AApT1B;AAqTI,QAAI,KAAK,aAAa,KAAK,gBAAiB,KAAiB,QAAQ,YAAY,MAAM,MAAM;AAC3F,aAAO,KAAK,CAAC,CAAC;AACd;AAAA,IACF;AAEA,iBAAO,OAAO,SAAS,CAAC,MAAxB,mBAA2B,KAAK,cAAc,IAAI;AAAA,EACpD,CAAC;AAED,SAAO,OACJ,IAAI,CAAC,UAAU,MAAM,KAAK,EAAE,CAAC,EAC7B,IAAI,CAAC,cAAc;AAClB,QAAI,QAAS,QAAO,aAAa,SAAS,SAAS;AACnD,WAAO,aAAa,QAAQ,SAAS,WAAW;AAAA,EAClD,CAAC,EACA,IAAI,eAAe,EACnB,OAAO,CAAC,YAAuC,QAAQ,OAAO,CAAC;AACpE;AAEA,SAAS,oBAAoB,SAAoC;AAC/D,QAAM,UAAU,QAAQ,QAAQ,YAAY;AAE5C,MAAI,mBAAmB,KAAK,QAAQ,SAAS,KAAK,eAAe,KAAK,QAAQ,SAAS,GAAG;AACxF,UAAMA,WAAU,gBAAgB,QAAQ,SAAS;AACjD,WAAOA,WAAU,CAACA,QAAO,IAAI,CAAC;AAAA,EAChC;AAEA,MAAI,YAAY,SAAS,oBAAoB,OAAO,GAAG;AACrD,UAAM,gBAAgB,eAAe,MAAM,KAAK,QAAQ,UAAU,CAAC;AACnE,WAAO,cAAc,SAAS,gBAAiB,CAAC,gBAAgB,QAAQ,SAAS,CAAC,EAAE,OAAO,OAAO;AAAA,EACpG;AAEA,MAAI,sBAAsB,IAAI,OAAO,KAAK,YAAY,MAAM,KAAK,QAAQ,UAAU,CAAC,KAAK,CAAC,oBAAoB,OAAO,GAAG;AACtH,WAAO,iBAAiB,MAAM,KAAK,QAAQ,UAAU,GAAG,OAAO;AAAA,EACjE;AAEA,QAAM,UAAU,gBAAgB,QAAQ,SAAS;AACjD,SAAO,UAAU,CAAC,OAAO,IAAI,CAAC;AAChC;AAEA,SAAS,eAAe,OAA0C;AAChE,QAAM,WAA6B,CAAC;AACpC,MAAI,cAAsB,CAAC;AAE3B,QAAM,cAAc,MAAM;AACxB,QAAI,CAAC,YAAY,OAAQ;AACzB,aAAS,KAAK,GAAG,iBAAiB,WAAW,CAAC;AAC9C,kBAAc,CAAC;AAAA,EACjB;AAEA,QAAM,QAAQ,CAAC,SAAS;AACtB,QAAI,KAAK,aAAa,KAAK,cAAc;AACvC,YAAM,UAAU;AAChB,YAAM,UAAU,QAAQ,QAAQ,YAAY;AAC5C,UAAI,WAAW,IAAI,OAAO,GAAG;AAC3B,oBAAY;AACZ,iBAAS,KAAK,GAAG,oBAAoB,OAAO,CAAC;AAC7C;AAAA,MACF;AAAA,IACF;AAEA,gBAAY,KAAK,IAAI;AAAA,EACvB,CAAC;AAED,cAAY;AACZ,SAAO;AACT;AAEA,SAAS,qBAAqB,MAAc,SAAiB,YAA4B;AACvF,MAAI,YAAY,KAAM,QAAO,aAAa;AAE1C,QAAM,aAAa,IAAI,OAAO,MAAM,OAAO,aAAa,IAAI;AAC5D,aAAW,YAAY,aAAa;AACpC,MAAI,QAAQ;AACZ,MAAI;AAEJ,UAAQ,QAAQ,WAAW,KAAK,IAAI,OAAO,MAAM;AAC/C,UAAM,SAAS,MAAM,CAAC;AACtB,QAAI,OAAO,KAAK,MAAM,EAAG,UAAS;AAAA,aACzB,CAAC,UAAU,KAAK,MAAM,EAAG,UAAS;AAC3C,QAAI,UAAU,EAAG,QAAO,WAAW;AAAA,EACrC;AAEA,SAAO,KAAK;AACd;AAEA,SAAS,0BAA0B,MAAgC;AACjE,QAAM,WAA6B,CAAC;AACpC,MAAI,SAAS;AAEb,QAAM,aAAa,CAAC,eAAuB;AACzC,UAAM,SAAS,WAAW,MAAM,SAAS;AACzC,UAAM,QAAQ,OAAO,SAAS;AAC9B,WAAO,QAAQ,CAAC,UAAU;AACxB,YAAM,UAAU,gBAAgB,QAAQ,QAAQ,KAAK,WAAW,KAAK;AACrE,UAAI,QAAS,UAAS,KAAK,OAAO;AAAA,IACpC,CAAC;AAAA,EACH;AAEA,SAAO,SAAS,KAAK,QAAQ;AAC3B,UAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,UAAM,QAAQ,oBAAoB,KAAK,IAAI;AAC3C,QAAI,CAAC,SAAS,MAAM,UAAU,QAAW;AACvC,iBAAW,IAAI;AACf;AAAA,IACF;AAEA,QAAI,MAAM,QAAQ,EAAG,YAAW,KAAK,MAAM,GAAG,MAAM,KAAK,CAAC;AAE1D,UAAM,WAAW,SAAS,MAAM;AAChC,UAAM,UAAU,MAAM,CAAC;AACvB,UAAM,UAAU,MAAM,CAAC,EAAE,YAAY;AACrC,UAAM,aAAa,WAAW,QAAQ,SAAS;AAC/C,UAAM,aAAa,qBAAqB,MAAM,SAAS,UAAU;AACjE,UAAM,YAAY,KAAK,MAAM,UAAU,UAAU;AAEjD,QAAI,sBAAsB,IAAI,OAAO,KAAK,UAAU,KAAK,SAAS,GAAG;AACnE,YAAM,UAAU;AAChB,YAAM,WAAW,KAAK,OAAO;AAC7B,YAAM,QAAQ,UAAU,QAAQ,IAAI,OAAO,IAAI,QAAQ,QAAQ,uBAAuB,MAAM,CAAC,IAAI,GAAG,GAAG,EAAE,EAAE,QAAQ,IAAI,OAAO,GAAG,QAAQ,KAAK,GAAG,GAAG,EAAE;AACtJ,YAAM,MAAM,SAAS,EAAE,QAAQ,CAAC,UAAU;AACxC,cAAM,UAAU,gBAAgB,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,EAAE;AAC/D,YAAI,QAAS,UAAS,KAAK,OAAO;AAAA,MACpC,CAAC;AAAA,IACH,OAAO;AACL,YAAM,UAAU,gBAAgB,SAAS;AACzC,UAAI,QAAS,UAAS,KAAK,OAAO;AAAA,IACpC;AAEA,aAAS;AAAA,EACX;AAEA,SAAO;AACT;AAEA,SAAS,mBAAmB,MAAgC;AAC1D,MAAI,OAAO,aAAa,eAAe,OAAO,SAAS,aAAa;AAClE,WAAO,0BAA0B,IAAI;AAAA,EACvC;AAEA,QAAM,WAAW,SAAS,cAAc,UAAU;AAClD,WAAS,YAAY;AACrB,SAAO,eAAe,MAAM,KAAK,SAAS,QAAQ,UAAU,CAAC;AAC/D;AAEA,SAAS,mBAAmB,MAAgC;AArc5D;AAscE,QAAM,aAAa,uBAAuB,IAAI;AAC9C,QAAM,SAAkB,gBAAW,MAAM,iBAAiB,MAAlC,YAAuC,CAAC;AAChE,SAAO,MACJ,OAAO,CAAC,MAAM,UAAU,KAAK,SAAS,KAAK,EAAE,UAAU,MAAM,SAAS,KAAK,SAAS,GAAG,EACvF,IAAI,CAAC,UAAU,EAAE,MAAM,MAAM,aAAa,KAAK,QAAQ,WAAW,GAAG,EAAE,KAAK,EAAE,EAAE;AACrF;AAEA,SAAS,iBAAiB,MAAsB;AA7chD;AA8cE,UAAO,gBAAK,QAAQ,WAAW,GAAG,EAAE,UAAU,EAAE,MAAM,OAAO,EAAE,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,MAAlF,mBAAqF,WAArF,YAA+F;AACxG;AAEA,SAAS,uBAAuB,MAAuB;AACrD,MAAI,CAAC,QAAQ,KAAK,SAAS,GAAI,QAAO;AACtC,MAAI,gCAAgC,KAAK,IAAI,EAAG,QAAO;AAEvD,QAAM,QAAQ,KAAK,MAAM,KAAK,EAAE,OAAO,OAAO;AAC9C,MAAI,MAAM,SAAS,KAAK,MAAM,SAAS,EAAG,QAAO;AAEjD,SAAO,MAAM,MAAM,CAAC,SAAS,sBAAsB,KAAK,IAAI,CAAC;AAC/D;AAEA,SAAS,uBAAuB,UAA4B,WAA2B;AA3dvF;AA4dE,WAAS,QAAQ,YAAY,GAAG,QAAQ,SAAS,QAAQ,SAAS,GAAG;AACnE,UAAM,OAAO,kBAAiB,oBAAS,KAAK,MAAd,mBAAiB,gBAAjB,YAAgC,EAAE;AAChE,QAAI,KAAM,QAAO;AAAA,EACnB;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,SAAkC;AAC/D,SAAO,QAAQ,QAAQ,SAAS,mBAAmB,KAAK,QAAQ,IAAI,KAAK,eAAe,KAAK,QAAQ,IAAI,EAAE;AAC7G;AAEA,SAAS,iBAAiB,UAA4B,OAAwB;AAC5E,QAAM,UAAU,SAAS,KAAK;AAC9B,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,sBAAsB,OAAO,EAAG,QAAO;AAE3C,QAAM,OAAO,iBAAiB,QAAQ,WAAW;AACjD,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,uBAAuB,KAAK,IAAI,KAAK,iBAAiB,KAAK,IAAI,KAAK,YAAY,KAAK,IAAI,EAAG,QAAO;AAEvG,QAAM,WAAW,uBAAuB,UAAU,KAAK;AACvD,MAAI,WAAW,KAAK,IAAI,EAAG,QAAO,QAAQ,aAAa,uBAAuB,QAAQ,KAAK,kBAAkB,KAAK,QAAQ,EAAE;AAE5H,SAAO,uBAAuB,IAAI,KAAK,kBAAkB,KAAK,QAAQ;AACxE;AAEA,SAAS,oBAAoB,UAAmG;AAC9H,QAAM,iBAAiB,SACpB,IAAI,CAAC,SAAS,UAAW,QAAQ,eAAe,sBAAsB,OAAO,IAAI,QAAQ,EAAG,EAC5F,OAAO,CAAC,UAAU,SAAS,CAAC;AAC/B,QAAM,eAAe,eAAe;AACpC,MAAI,eAAe,EAAG,QAAO,EAAE,cAAc,UAAU,iBAAiB,CAAC,EAAE;AAE3E,QAAM,gBAAgB,KAAK,IAAI,KAAK,KAAK,eAAe,GAAG,GAAG,CAAC;AAC/D,QAAM,uBAAuB,KAAK,IAAI,GAAG,eAAe,aAAa;AAErE,WAAS,UAAU,sBAAsB,UAAU,cAAc,WAAW,GAAG;AAC7E,UAAM,QAAQ,eAAe,OAAO;AACpC,QAAI,UAAU,KAAK,iBAAiB,UAAU,KAAK,GAAG;AACpD,aAAO,EAAE,cAAc,SAAS,MAAM,GAAG,KAAK,GAAG,iBAAiB,SAAS,MAAM,KAAK,EAAE;AAAA,IAC1F;AAAA,EACF;AAEA,SAAO,EAAE,cAAc,UAAU,iBAAiB,CAAC,EAAE;AACvD;AAEO,SAAS,yBAAyB,MAAoC;AAC3E,QAAM,gBAAgB,aAAa,oBAAoB,IAAI,CAAC;AAC5D,QAAM,EAAE,cAAc,gBAAgB,IAAI,oBAAoB,mBAAmB,aAAa,CAAC;AAE/F,MAAI,CAAC,gBAAgB,OAAQ,QAAO,EAAE,UAAU,eAAe,aAAa,GAAG;AAE/E,SAAO;AAAA,IACL,UAAU,aAAa,IAAI,CAAC,YAAS;AAjhBzC;AAihB4C,2BAAQ,SAAR,YAAgB;AAAA,KAAE,EAAE,KAAK,EAAE;AAAA,IACnE,aAAa,gBAAgB,IAAI,CAAC,YAAS;AAlhB/C;AAkhBkD,2BAAQ,SAAR,YAAgB;AAAA,KAAE,EAAE,KAAK,EAAE;AAAA,EAC3E;AACF;AAEO,SAAS,yBAAyB,MAAoC;AAC3E,QAAM,cAAc,uBAAuB,IAAI;AAC/C,QAAM,EAAE,cAAc,gBAAgB,IAAI,oBAAoB,mBAAmB,WAAW,CAAC;AAE7F,MAAI,CAAC,gBAAgB,OAAQ,QAAO,EAAE,UAAU,aAAa,aAAa,GAAG;AAE7E,SAAO;AAAA,IACL,UAAU,aAAa,IAAI,CAAC,YAAS;AA7hBzC;AA6hB4C,2BAAQ,SAAR,YAAgB;AAAA,KAAE,EAAE,KAAK,EAAE;AAAA,IACnE,aAAa,gBAAgB,IAAI,CAAC,YAAS;AA9hB/C;AA8hBkD,2BAAQ,SAAR,YAAgB;AAAA,KAAE,EAAE,KAAK,EAAE;AAAA,EAC3E;AACF;AAEO,SAAS,iBAAiB,OAA2D,YAAY,KAAa;AAliBrH;AAmiBE,QAAM,QAAO,WAAM,aAAN,mBAAgB;AAC7B,MAAI,MAAM;AACR,WAAO,uBAAuB,kBAAkB,yBAAyB,IAAI,EAAE,UAAU,SAAS,CAAC,EAAE,KAAK;AAAA,EAC5G;AAEA,QAAM,QAAO,WAAM,SAAN,YAAc;AAC3B,QAAM,WAAW,yBAAyB,IAAI,EAAE;AAChD,QAAM,aAAY,oBAAS,MAAM,IAAI,EAAE,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,MAA/C,mBAAkD,WAAlD,YAA4D;AAC9E,SAAO,uBAAuB,SAAS,EAAE,QAAQ,QAAQ,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS;AACzF;","names":["segment"]}
|
|
@@ -2,16 +2,19 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
interface EmailPreviewCardProps {
|
|
4
4
|
from: {
|
|
5
|
-
name
|
|
6
|
-
email
|
|
5
|
+
name?: string | null;
|
|
6
|
+
email?: string | null;
|
|
7
7
|
};
|
|
8
|
-
to?: string;
|
|
8
|
+
to?: string | string[] | null;
|
|
9
|
+
cc?: string | string[] | null;
|
|
10
|
+
bcc?: string | string[] | null;
|
|
9
11
|
subject?: string;
|
|
10
12
|
htmlBody?: string;
|
|
11
13
|
textBody?: string;
|
|
12
14
|
signatureHtml?: string | null;
|
|
15
|
+
signatureText?: string | null;
|
|
13
16
|
className?: string;
|
|
14
17
|
}
|
|
15
|
-
declare function EmailPreviewCard({ from, to, subject, htmlBody, textBody, signatureHtml, className, }: EmailPreviewCardProps): React.JSX.Element;
|
|
18
|
+
declare function EmailPreviewCard({ from, to, cc, bcc, subject, htmlBody, textBody, signatureHtml, signatureText, className, }: EmailPreviewCardProps): React.JSX.Element;
|
|
16
19
|
|
|
17
20
|
export { EmailPreviewCard, type EmailPreviewCardProps };
|
|
@@ -4,23 +4,43 @@
|
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { Eye } from "lucide-react";
|
|
6
6
|
import { cn } from "../lib/utils.js";
|
|
7
|
+
import { EmailBody } from "./email-body.js";
|
|
8
|
+
import { decodeEmailDisplayText, formatAddressList, normalizeEmailSender } from "./email-display-helpers.js";
|
|
7
9
|
function getInitials(name) {
|
|
8
10
|
return name.split(" ").map((part) => part[0]).filter(Boolean).slice(0, 2).join("").toUpperCase();
|
|
9
11
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
+
function formatRecipientLabel(to) {
|
|
13
|
+
const formatted = formatAddressList(to);
|
|
14
|
+
if (!formatted) return "the recipient's";
|
|
15
|
+
if (Array.isArray(to) && to.length > 1) return "the recipients'";
|
|
16
|
+
return `${formatted}'s`;
|
|
17
|
+
}
|
|
18
|
+
function RecipientRow({ label, value }) {
|
|
19
|
+
if (!value) return null;
|
|
20
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 text-xs text-muted-foreground", children: [
|
|
21
|
+
/* @__PURE__ */ jsx("span", { className: "w-8 shrink-0 font-medium text-muted-foreground/70", children: label }),
|
|
22
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 break-words", children: value })
|
|
23
|
+
] });
|
|
12
24
|
}
|
|
13
25
|
function EmailPreviewCard({
|
|
14
26
|
from,
|
|
15
27
|
to,
|
|
28
|
+
cc,
|
|
29
|
+
bcc,
|
|
16
30
|
subject,
|
|
17
31
|
htmlBody,
|
|
18
32
|
textBody,
|
|
19
33
|
signatureHtml,
|
|
34
|
+
signatureText,
|
|
20
35
|
className
|
|
21
36
|
}) {
|
|
22
|
-
|
|
23
|
-
const
|
|
37
|
+
var _a;
|
|
38
|
+
const sender = normalizeEmailSender({ name: from.name, email: from.email, fallbackName: (_a = from.email) != null ? _a : "Unknown sender" });
|
|
39
|
+
const toLabel = formatAddressList(to);
|
|
40
|
+
const ccLabel = formatAddressList(cc);
|
|
41
|
+
const bccLabel = formatAddressList(bcc);
|
|
42
|
+
const recipientLabel = formatRecipientLabel(to);
|
|
43
|
+
const subjectLabel = subject ? decodeEmailDisplayText(subject) : "(no subject)";
|
|
24
44
|
return /* @__PURE__ */ jsxs("div", { className: cn("p-4 bg-muted/30 min-h-full", className), children: [
|
|
25
45
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 mb-3 py-2.5 px-3 border rounded-lg bg-background text-[11.5px] text-muted-foreground", children: [
|
|
26
46
|
/* @__PURE__ */ jsx(Eye, { className: "size-4 shrink-0 mt-0.5" }),
|
|
@@ -31,37 +51,40 @@ function EmailPreviewCard({
|
|
|
31
51
|
] })
|
|
32
52
|
] }),
|
|
33
53
|
/* @__PURE__ */ jsxs("div", { className: "bg-background border rounded-xl shadow-sm overflow-hidden", children: [
|
|
34
|
-
/* @__PURE__ */ jsx("div", { className: "px-[18px] pt-4 pb-3 text-base font-semibold border-b border-border/50", children:
|
|
35
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-
|
|
36
|
-
/* @__PURE__ */ jsx("div", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-foreground text-background text-xs font-semibold", children: getInitials(
|
|
54
|
+
/* @__PURE__ */ jsx("div", { className: "px-[18px] pt-4 pb-3 text-base font-semibold border-b border-border/50", children: subjectLabel }),
|
|
55
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 px-[18px] pt-3", children: [
|
|
56
|
+
/* @__PURE__ */ jsx("div", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-foreground text-background text-xs font-semibold", children: getInitials(sender.name || sender.email || "?") || "?" }),
|
|
37
57
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
38
58
|
/* @__PURE__ */ jsxs("div", { className: "text-sm", children: [
|
|
39
|
-
/* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children:
|
|
59
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: sender.name }),
|
|
40
60
|
" ",
|
|
41
|
-
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
61
|
+
sender.email ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
42
62
|
"<",
|
|
43
|
-
|
|
63
|
+
sender.email,
|
|
44
64
|
">"
|
|
45
|
-
] })
|
|
65
|
+
] }) : null
|
|
46
66
|
] }),
|
|
47
|
-
/* @__PURE__ */
|
|
67
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 space-y-0.5", children: [
|
|
68
|
+
/* @__PURE__ */ jsx(RecipientRow, { label: "To", value: toLabel || "no recipient yet" }),
|
|
69
|
+
/* @__PURE__ */ jsx(RecipientRow, { label: "Cc", value: ccLabel }),
|
|
70
|
+
/* @__PURE__ */ jsx(RecipientRow, { label: "Bcc", value: bccLabel })
|
|
71
|
+
] })
|
|
48
72
|
] }),
|
|
49
|
-
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground shrink-0", children: "just now" })
|
|
73
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground shrink-0 pt-0.5", children: "just now" })
|
|
50
74
|
] }),
|
|
51
|
-
/* @__PURE__ */ jsx(
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
className: "px-[18px] py-2 ml-[47px] text-[13.5px] leading-relaxed whitespace-pre-wrap",
|
|
55
|
-
dangerouslySetInnerHTML: { __html: bodyHtml }
|
|
56
|
-
}
|
|
57
|
-
),
|
|
58
|
-
signatureHtml ? /* @__PURE__ */ jsx(
|
|
59
|
-
"div",
|
|
75
|
+
/* @__PURE__ */ jsx("div", { className: "px-[18px] py-3 ml-[47px]", children: /* @__PURE__ */ jsx(
|
|
76
|
+
EmailBody,
|
|
60
77
|
{
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
html: htmlBody,
|
|
79
|
+
text: textBody,
|
|
80
|
+
detailsHtml: signatureHtml,
|
|
81
|
+
detailsText: signatureText,
|
|
82
|
+
variant: "preview",
|
|
83
|
+
collapseDetails: false,
|
|
84
|
+
defaultDetailsOpen: true,
|
|
85
|
+
className: "text-[13.5px]"
|
|
63
86
|
}
|
|
64
|
-
)
|
|
87
|
+
) })
|
|
65
88
|
] })
|
|
66
89
|
] });
|
|
67
90
|
}
|