discord-html-transcripts-fix 1.1.0 → 1.3.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.
@@ -22,8 +22,13 @@ function ComponentRow({ component, id, context, }) {
22
22
  switch (component.type) {
23
23
  case discord_js_1.ComponentType.ActionRow:
24
24
  return ((0, jsx_runtime_1.jsx)("discord-action-row", { children: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: component.components.map((nestedComponent, id) => ((0, jsx_runtime_1.jsx)(Component, { component: nestedComponent, id: id }, id))) }) }, id));
25
- case discord_js_1.ComponentType.Container:
26
- return ((0, jsx_runtime_1.jsx)(Container_1.default, { children: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: component.components.map((nestedComponent, id) => ((0, jsx_runtime_1.jsx)(ComponentRow, { component: nestedComponent, id: id, context: context }, id))) }) }, id));
25
+ case discord_js_1.ComponentType.Container: {
26
+ const accentColor = component.accent_color !== undefined
27
+ ? component.accent_color
28
+ : (component.accentColor !== undefined ? component.accentColor : null);
29
+ const spoiler = !!component.spoiler;
30
+ return ((0, jsx_runtime_1.jsx)(Container_1.default, { accentColor: accentColor, spoiler: spoiler, children: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: component.components.map((nestedComponent, id) => ((0, jsx_runtime_1.jsx)(ComponentRow, { component: nestedComponent, id: id, context: context }, id))) }) }, id));
31
+ }
27
32
  case discord_js_1.ComponentType.File: {
28
33
  const attachmentComponent = (0, jsx_runtime_1.jsx)("discord-file-attachment", { href: component.file.url });
29
34
  if (component.spoiler) {
@@ -12,6 +12,7 @@ const discord_markdown_parser_1 = __importDefault(require("discord-markdown-pars
12
12
  const discord_js_1 = require("discord.js");
13
13
  const utils_1 = require("../../utils/utils");
14
14
  const DiscordHighlightedCode_1 = require("./components/DiscordHighlightedCode");
15
+
15
16
  var RenderType;
16
17
  (function (RenderType) {
17
18
  RenderType[RenderType["EMBED"] = 0] = "EMBED";
@@ -19,158 +20,335 @@ var RenderType;
19
20
  RenderType[RenderType["NORMAL"] = 2] = "NORMAL";
20
21
  RenderType[RenderType["WEBHOOK"] = 3] = "WEBHOOK";
21
22
  })(RenderType || (exports.RenderType = RenderType = {}));
22
- /**
23
- * Renders discord markdown content
24
- * @param content - The content to render
25
- * @param context - The context to render the content in
26
- * @returns
27
- */
23
+
24
+ function hexToRgba(hex, alpha) {
25
+ if (typeof hex !== 'string') return null;
26
+ let h = hex.replace(/^#/, '');
27
+ if (h.length === 3) h = h.split('').map(c => c + c).join('');
28
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
29
+ const r = parseInt(h.slice(0, 2), 16);
30
+ const g = parseInt(h.slice(2, 4), 16);
31
+ const b = parseInt(h.slice(4, 6), 16);
32
+ return `rgba(${r},${g},${b},${alpha})`;
33
+ }
34
+
35
+ // Discord-specific block-level markdown that discord-markdown-parser misses in inline mode.
36
+ // Run BEFORE the parser, extract heading/subtext lines, parse the rest, splice back together.
37
+ const HEADING_RE = /^(#{1,3})\s+(.+?)\s*$/;
38
+ const SUBTEXT_RE = /^-#\s+(.+?)\s*$/;
39
+ const CODE_FENCE_RE = /^```([a-zA-Z0-9_+-]*)\s*\n([\s\S]*?)```$/m;
40
+
41
+ function preParseDiscord(content, mode) {
42
+ if (!content || typeof content !== 'string') return [];
43
+ const lines = content.split('\n');
44
+ const nodes = [];
45
+ let inlineBuffer = '';
46
+ let codeFenceActive = null; // {lang, lines}
47
+
48
+ const flushInline = () => {
49
+ if (!inlineBuffer) return;
50
+ const parsed = discord_markdown_parser_1.default(inlineBuffer, mode);
51
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
52
+ nodes.push(...arr);
53
+ inlineBuffer = '';
54
+ };
55
+
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const line = lines[i];
58
+
59
+ // Code fence — close only on `^\s*\`\`\`\s*$` (Discord-style: triple-backticks alone on line)
60
+ if (codeFenceActive) {
61
+ if (/^\s*```\s*$/.test(line)) {
62
+ nodes.push({ type: 'codeBlock', lang: codeFenceActive.lang || '', content: codeFenceActive.lines.join('\n') });
63
+ codeFenceActive = null;
64
+ } else {
65
+ codeFenceActive.lines.push(line);
66
+ }
67
+ continue;
68
+ }
69
+ const fenceMatch = line.match(/^```([a-zA-Z0-9_+-]*)\s*$/);
70
+ if (fenceMatch) {
71
+ flushInline();
72
+ codeFenceActive = { lang: fenceMatch[1], lines: [] };
73
+ continue;
74
+ }
75
+
76
+ const h = line.match(HEADING_RE);
77
+ if (h) {
78
+ flushInline();
79
+ const level = h[1].length;
80
+ const headingInner = discord_markdown_parser_1.default(h[2], mode);
81
+ nodes.push({ type: 'heading', level, content: Array.isArray(headingInner) ? headingInner : [headingInner] });
82
+ continue;
83
+ }
84
+
85
+ const s = line.match(SUBTEXT_RE);
86
+ if (s) {
87
+ flushInline();
88
+ const innerNodes = discord_markdown_parser_1.default(s[1], mode);
89
+ nodes.push({ type: 'subtext', content: Array.isArray(innerNodes) ? innerNodes : [innerNodes] });
90
+ continue;
91
+ }
92
+
93
+ if (inlineBuffer) inlineBuffer += '\n';
94
+ inlineBuffer += line;
95
+ }
96
+ if (codeFenceActive) {
97
+ // unclosed fence — flush as text-ish
98
+ inlineBuffer += '```' + (codeFenceActive.lang ? codeFenceActive.lang : '') + '\n' + codeFenceActive.lines.join('\n');
99
+ }
100
+ flushInline();
101
+ return nodes;
102
+ }
103
+
28
104
  async function MessageContent({ content, context }) {
29
- if (context.type === RenderType.REPLY && content.length > 180)
105
+ if (context.type === RenderType.REPLY && content.length > 180) {
30
106
  content = content.slice(0, 180) + '...';
31
- // parse the markdown
32
- const parsed = (0, discord_markdown_parser_1.default)(content, context.type === RenderType.EMBED || context.type === RenderType.WEBHOOK ? 'extended' : 'normal');
33
- // check if the parsed content is only emojis
34
- const isOnlyEmojis = parsed.every((node) => ['emoji', 'twemoji'].includes(node.type) || (node.type === 'text' && node.content.trim().length === 0));
107
+ }
108
+ const mode = context.type === RenderType.EMBED || context.type === RenderType.WEBHOOK ? 'extended' : 'normal';
109
+ // REPLY mode is always inline-only (heading inside a reply preview makes no sense).
110
+ // For all other modes (NORMAL/EMBED/WEBHOOK) we pre-process headings/subtext/code-fences
111
+ // so they render correctly even though discord-markdown-parser is inline-only.
112
+ const parsed = context.type === RenderType.REPLY
113
+ ? discord_markdown_parser_1.default(content, mode)
114
+ : preParseDiscord(content, mode);
115
+
116
+ const nodes = Array.isArray(parsed) ? parsed : [parsed];
117
+ const isOnlyEmojis = nodes.every((node) => ['emoji', 'twemoji'].includes(node.type) || (node.type === 'text' && node.content.trim().length === 0));
35
118
  if (isOnlyEmojis) {
36
- // now check if there are less than or equal to 25 emojis
37
- const emojis = parsed.filter((node) => ['emoji', 'twemoji'].includes(node.type));
119
+ const emojis = nodes.filter((node) => ['emoji', 'twemoji'].includes(node.type));
38
120
  if (emojis.length <= 25) {
39
- context._internal = {
40
- largeEmojis: true,
41
- };
121
+ context._internal = { largeEmojis: true };
42
122
  }
43
123
  }
44
- return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: parsed, context: context });
124
+ return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes, context });
45
125
  }
46
- // This function can probably be combined into the MessageSingleASTNode function
47
- async function MessageASTNodes({ nodes, context, }) {
126
+
127
+ async function MessageASTNodes({ nodes, context }) {
48
128
  if (Array.isArray(nodes)) {
49
- return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: nodes.map((node, i) => ((0, jsx_runtime_1.jsx)(MessageSingleASTNode, { node: node, context: context }, i))) }));
50
- }
51
- else {
52
- return (0, jsx_runtime_1.jsx)(MessageSingleASTNode, { node: nodes, context: context });
129
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: nodes.map((node, i) => (0, jsx_runtime_1.jsx)(MessageSingleASTNode, { node, context }, i)) });
53
130
  }
131
+ return (0, jsx_runtime_1.jsx)(MessageSingleASTNode, { node: nodes, context });
54
132
  }
133
+
55
134
  async function MessageSingleASTNode({ node, context }) {
56
- var _a, _b;
57
- if (!node)
58
- return null;
135
+ if (!node) return null;
59
136
  try {
60
137
  return await renderASTNode(node, context);
61
- }
62
- catch (err) {
63
- console.warn('[discord-html-transcripts-fix] Failed to render AST node, falling back to raw:', err && err.message ? err.message : err);
64
- // Fallback: render as plain text from node.content if possible
138
+ } catch (err) {
139
+ console.warn('[discord-html-transcripts-fix] AST node render failed:', err && err.message ? err.message : err);
65
140
  try {
66
141
  if (typeof node.content === 'string') return node.content;
67
- if (Array.isArray(node.content)) return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context });
68
- } catch (_e) { /* ignore */ }
142
+ if (Array.isArray(node.content)) return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context });
143
+ } catch (_e) {}
69
144
  return null;
70
145
  }
71
146
  }
147
+
148
+ const GUIDE_LABELS = {
149
+ 'guide': 'Server Guide',
150
+ 'browse': 'Channels & Roles',
151
+ 'customize': 'Customize',
152
+ 'welcome': 'Welcome',
153
+ 'linked-roles': 'Linked Roles',
154
+ 'home': 'Home',
155
+ 'members': 'Members',
156
+ 'emoji': 'Emoji',
157
+ 'soundboard': 'Soundboard',
158
+ 'shop': 'Shop',
159
+ 'events': 'Events',
160
+ 'channel': 'Channels',
161
+ };
162
+
72
163
  async function renderASTNode(node, context) {
73
- var _a, _b;
74
164
  const type = node.type;
75
165
  switch (type) {
76
166
  case 'text':
77
167
  return node.content;
78
168
  case 'link':
79
- return ((0, jsx_runtime_1.jsx)("discord-link", { href: node.target, children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
169
+ return (0, jsx_runtime_1.jsx)("discord-link", { href: (0, utils_1.safeHref)(node.target), children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
80
170
  case 'url':
81
171
  case 'autolink':
82
- return ((0, jsx_runtime_1.jsx)("discord-link", { href: node.target, target: "_blank", rel: "noreferrer", children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
172
+ return (0, jsx_runtime_1.jsx)("discord-link", { href: (0, utils_1.safeHref)(node.target), target: "_blank", rel: "noreferrer", children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
83
173
  case 'blockQuote':
84
174
  if (context.type === RenderType.REPLY) {
85
- return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context });
175
+ return (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context });
86
176
  }
87
- return ((0, jsx_runtime_1.jsx)("discord-quote", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
177
+ return (0, jsx_runtime_1.jsx)("discord-quote", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
88
178
  case 'br':
89
179
  case 'newline':
90
- if (context.type === RenderType.REPLY)
91
- return ' ';
180
+ if (context.type === RenderType.REPLY) return ' ';
92
181
  return (0, jsx_runtime_1.jsx)("br", {});
182
+
183
+ // Headings: #/##/### in Discord. The discord-markdown-parser doesn't emit these
184
+ // in inline mode, so preParseDiscord extracts them before the parser sees them.
185
+ case 'heading': {
186
+ const level = Math.max(1, Math.min(3, node.level || 1));
187
+ const Tag = level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3';
188
+ return (0, jsx_runtime_1.jsx)(Tag, { className: `dht-heading dht-heading-${level}`, children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
189
+ }
190
+ // Subtext: lines starting with `-# ` are small/dim text
191
+ case 'subtext':
192
+ return (0, jsx_runtime_1.jsx)("small", { className: "dht-subtext", children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
193
+
93
194
  case 'channel': {
94
195
  const id = node.id;
95
- const channel = await context.callbacks.resolveChannel(id);
96
- return ((0, jsx_runtime_1.jsx)("discord-mention", { type: channel ? (channel.isDMBased() ? 'channel' : getChannelType(channel.type)) : 'channel', "data-mention-type": "channel", "data-mention-id": id, className: "dht-mention", children: channel ? (channel.isDMBased() ? 'DM Channel' : channel.name) : `<#${id}>` }));
196
+ if (typeof id === 'string' && /^[a-z][a-z-]*$/.test(id)) {
197
+ const label = GUIDE_LABELS[id] || id;
198
+ return (0, jsx_runtime_1.jsxs)("span", { className: "dht-mention dht-mention--channel", children: ["#", label] });
199
+ }
200
+ const channel = await context.callbacks.resolveChannel(id).catch(() => null);
201
+ const name = channel ? (channel.isDMBased() ? 'DM Channel' : channel.name) : `unknown-${id.slice(-6)}`;
202
+ return (0, jsx_runtime_1.jsxs)("span", {
203
+ className: "dht-mention dht-mention--channel",
204
+ "data-mention-type": "channel",
205
+ "data-mention-id": id,
206
+ children: ["#", name]
207
+ });
97
208
  }
209
+
98
210
  case 'role': {
99
211
  const id = node.id;
100
- const role = await context.callbacks.resolveRole(id);
101
- return ((0, jsx_runtime_1.jsx)("discord-mention", { type: "role", color: context.type === RenderType.REPLY ? undefined : role === null || role === void 0 ? void 0 : role.hexColor, "data-mention-type": "role", "data-mention-id": id, className: "dht-mention", children: role ? role.name : `<@&${id}>` }));
212
+ const role = await context.callbacks.resolveRole(id).catch(() => null);
213
+ const name = role?.name ?? `role-${id.slice(-6)}`;
214
+ const color = role?.hexColor && role.hexColor !== '#000000' ? (0, utils_1.safeColor)(role.hexColor, undefined) : undefined;
215
+ const bg = color ? hexToRgba(color, 0.18) : undefined;
216
+ const style = color ? { color, backgroundColor: bg, borderColor: hexToRgba(color, 0.45) } : undefined;
217
+ return (0, jsx_runtime_1.jsxs)("span", {
218
+ className: "dht-mention dht-mention--role",
219
+ "data-mention-type": "role",
220
+ "data-mention-id": id,
221
+ style,
222
+ children: ["@", name]
223
+ });
102
224
  }
225
+
103
226
  case 'user': {
104
227
  const id = node.id;
105
- const user = await context.callbacks.resolveUser(id);
106
- return (0, jsx_runtime_1.jsx)("discord-mention", { type: "user", "data-mention-type": "user", "data-mention-id": id, className: "dht-mention", children: user ? ((_a = user.displayName) !== null && _a !== void 0 ? _a : user.username) : `<@${id}>` });
228
+ const user = await context.callbacks.resolveUser(id).catch(() => null);
229
+ const name = user ? (user.displayName ?? user.username ?? `user-${id.slice(-6)}`) : `user-${id.slice(-6)}`;
230
+ return (0, jsx_runtime_1.jsxs)("span", {
231
+ className: "dht-mention dht-mention--user",
232
+ "data-mention-type": "user",
233
+ "data-mention-id": id,
234
+ children: ["@", name]
235
+ });
236
+ }
237
+
238
+ case 'slashCommand':
239
+ case 'command': {
240
+ const name = node.name || node.content || 'command';
241
+ return (0, jsx_runtime_1.jsxs)("span", { className: "dht-mention dht-mention--command", children: ["/", String(name)] });
107
242
  }
243
+
244
+ case 'guildNavigation': {
245
+ // discord-markdown-parser emits this for <id:guide> etc.
246
+ const id = node.id || node.content || 'guide';
247
+ const label = GUIDE_LABELS[id] || id;
248
+ return (0, jsx_runtime_1.jsxs)("span", { className: "dht-mention dht-mention--channel", children: ["#", label] });
249
+ }
250
+
108
251
  case 'here':
109
252
  case 'everyone':
110
- return ((0, jsx_runtime_1.jsx)("discord-mention", { type: 'role', highlight: true, children: `@${type}` }));
253
+ return (0, jsx_runtime_1.jsxs)("span", { className: "dht-mention dht-mention--role dht-mention--highlight", children: ["@", type] });
254
+
111
255
  case 'codeBlock':
112
256
  if (context.type !== RenderType.REPLY) {
113
257
  return (0, jsx_runtime_1.jsx)(DiscordHighlightedCode_1.DiscordHighlightedCode, { language: node.lang, content: node.content });
114
258
  }
115
- return (0, jsx_runtime_1.jsx)("discord-code", { children: node.content });
259
+ return (0, jsx_runtime_1.jsx)("code", { className: "dht-inline-code", children: node.content });
116
260
  case 'inlineCode':
117
- return (0, jsx_runtime_1.jsx)("discord-code", { children: node.content });
261
+ return (0, jsx_runtime_1.jsx)("code", { className: "dht-inline-code", children: node.content });
262
+
118
263
  case 'em':
119
- return ((0, jsx_runtime_1.jsx)("discord-italic", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
264
+ return (0, jsx_runtime_1.jsx)("em", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
120
265
  case 'strong':
121
- return ((0, jsx_runtime_1.jsx)("discord-bold", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
266
+ return (0, jsx_runtime_1.jsx)("strong", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
122
267
  case 'underline':
123
- return ((0, jsx_runtime_1.jsx)("discord-underlined", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
268
+ return (0, jsx_runtime_1.jsx)("u", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
124
269
  case 'strikethrough':
125
- return ((0, jsx_runtime_1.jsx)("s", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
270
+ return (0, jsx_runtime_1.jsx)("s", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
126
271
  case 'emoticon':
127
- return typeof node.content === 'string' ? (node.content) : ((0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }));
272
+ return typeof node.content === 'string' ? node.content : (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context });
128
273
  case 'spoiler':
129
- return ((0, jsx_runtime_1.jsx)("discord-spoiler", { children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }) }));
274
+ return (0, jsx_runtime_1.jsx)("span", { className: "discord-spoiler", children: (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context }) });
275
+
130
276
  case 'emoji':
131
- case 'twemoji':
132
- return ((0, jsx_runtime_1.jsx)("discord-custom-emoji", { name: node.name, url: (0, utils_1.parseDiscordEmoji)(node), embedEmoji: context.type === RenderType.EMBED, jumbo: (_b = context._internal) === null || _b === void 0 ? void 0 : _b.largeEmojis }));
277
+ case 'twemoji': {
278
+ const url = (0, utils_1.parseDiscordEmoji)(node);
279
+ const jumbo = context._internal?.largeEmojis;
280
+ return (0, jsx_runtime_1.jsx)("img", {
281
+ className: jumbo ? "dht-emoji dht-emoji--jumbo" : "dht-emoji",
282
+ src: url,
283
+ alt: node.name || '',
284
+ draggable: false
285
+ });
286
+ }
287
+
133
288
  case 'timestamp': {
134
- // TODO: Make this reactive
135
- // https://github.com/ItzDerock/discord-components/blob/main/packages/core/src/components/discord-time/discord-time.tsx
136
- // Defensive parsing: Discord timestamps are Unix seconds (string oder number),
137
- // ungültige Werte (z.B. <t:abc:F> oder absurd große Zahlen) sollen kein RangeError werfen.
138
- var raw = node.timestamp;
139
- var dateMs = NaN;
289
+ const raw = node.timestamp;
290
+ let dateMs = NaN;
140
291
  if (typeof raw === 'number') {
141
292
  dateMs = raw < 1e12 ? raw * 1000 : raw;
142
293
  } else if (typeof raw === 'string') {
143
- var n = parseInt(raw, 10);
294
+ const n = parseInt(raw, 10);
144
295
  if (Number.isFinite(n)) dateMs = n < 1e12 ? n * 1000 : n;
145
296
  }
146
- var dt = new Date(dateMs);
147
- var iso = '';
148
- try { iso = Number.isFinite(dt.getTime()) ? dt.toISOString() : ''; } catch (_e) { iso = ''; }
149
- return (0, jsx_runtime_1.jsx)("discord-time", { children: iso });
297
+ const dt = new Date(dateMs);
298
+ if (!Number.isFinite(dt.getTime())) return (0, jsx_runtime_1.jsx)("span", { className: "dht-timestamp", children: "—" });
299
+ const fmt = formatTimestamp(dt, node.format);
300
+ return (0, jsx_runtime_1.jsx)("time", { className: "dht-timestamp", dateTime: dt.toISOString(), title: dt.toISOString(), children: fmt });
150
301
  }
151
- // return <DiscordTime timestamp={parseInt(node.timestamp) * 1000} format={node.format} />;
152
- default: {
153
- // console.log(`[discord-html-transcripts] Unknown node type: ${type}`, node);
154
- return typeof node.content === 'string' ? (node.content) : ((0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context: context }));
302
+
303
+ default:
304
+ return typeof node.content === 'string' ? node.content : (0, jsx_runtime_1.jsx)(MessageASTNodes, { nodes: node.content, context });
305
+ }
306
+ }
307
+
308
+ function formatTimestamp(dt, format) {
309
+ // Discord formats: t (short time), T (long time), d (short date), D (long date), f (short datetime), F (long datetime), R (relative)
310
+ try {
311
+ if (format === 'R') {
312
+ const diff = (dt.getTime() - Date.now()) / 1000;
313
+ const abs = Math.abs(diff);
314
+ const past = diff < 0;
315
+ const pluralize = (n, unit) => Math.round(n) + ' ' + unit + (Math.round(n) !== 1 ? 's' : '');
316
+ let val;
317
+ if (abs < 60) val = pluralize(abs, 'second');
318
+ else if (abs < 3600) val = pluralize(abs / 60, 'minute');
319
+ else if (abs < 86400) val = pluralize(abs / 3600, 'hour');
320
+ else if (abs < 2592000) val = pluralize(abs / 86400, 'day');
321
+ else if (abs < 31536000) val = pluralize(abs / 2592000, 'month');
322
+ else val = pluralize(abs / 31536000, 'year');
323
+ return past ? `${val} ago` : `in ${val}`;
155
324
  }
325
+ switch (format) {
326
+ case 't': return dt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
327
+ case 'T': return dt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
328
+ case 'd': return dt.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric' });
329
+ case 'D': return dt.toLocaleDateString('en-US', { day: 'numeric', month: 'long', year: 'numeric' });
330
+ case 'F': return dt.toLocaleString('en-US', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' });
331
+ case 'f':
332
+ default: return dt.toLocaleString('en-US', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' });
333
+ }
334
+ } catch (_e) {
335
+ return dt.toISOString();
156
336
  }
157
337
  }
338
+
158
339
  function getChannelType(channelType) {
159
340
  switch (channelType) {
160
341
  case discord_js_1.ChannelType.GuildCategory:
161
342
  case discord_js_1.ChannelType.GuildAnnouncement:
162
343
  case discord_js_1.ChannelType.GuildText:
163
344
  case discord_js_1.ChannelType.DM:
164
- case discord_js_1.ChannelType.GroupDM:
165
- case discord_js_1.ChannelType.GuildDirectory:
166
- case discord_js_1.ChannelType.GuildMedia:
167
345
  return 'channel';
168
346
  case discord_js_1.ChannelType.GuildVoice:
169
347
  case discord_js_1.ChannelType.GuildStageVoice:
170
348
  return 'voice';
349
+ case discord_js_1.ChannelType.AnnouncementThread:
171
350
  case discord_js_1.ChannelType.PublicThread:
172
351
  case discord_js_1.ChannelType.PrivateThread:
173
- case discord_js_1.ChannelType.AnnouncementThread:
174
352
  return 'thread';
175
353
  case discord_js_1.ChannelType.GuildForum:
176
354
  return 'forum';
@@ -178,4 +356,4 @@ function getChannelType(channelType) {
178
356
  return 'channel';
179
357
  }
180
358
  }
181
- //# sourceMappingURL=content.js.map
359
+ //# sourceMappingURL=content.js.map
@@ -1,44 +1,89 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
2
  var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
3
+ var ownKeys = function(o){ownKeys = Object.getOwnPropertyNames || function (o){var ar=[];for (var k in o) if (Object.prototype.hasOwnProperty.call(o,k)) ar[ar.length]=k;return ar;};return ownKeys(o);};
4
+ return function (mod){if (mod && mod.__esModule) return mod;var result={};if (mod != null) for (var k=ownKeys(mod), i=0; i<k.length; i++) if (k[i] !== "default") Object.defineProperty(result,k[i],{get:function(){return mod[k[i]];}});Object.defineProperty(result,"default",{value:mod,enumerable:true});return result;};
34
5
  })();
35
6
  Object.defineProperty(exports, "__esModule", { value: true });
36
7
  exports.DiscordEmbed = DiscordEmbed;
37
8
  const jsx_runtime_1 = require("react/jsx-runtime");
38
9
  const embeds_1 = require("../../utils/embeds");
39
10
  const content_1 = __importStar(require("./content"));
11
+ const utils_1 = require("../../utils/utils");
12
+
40
13
  async function DiscordEmbed({ embed, context }) {
41
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
42
- return ((0, jsx_runtime_1.jsxs)("discord-embed", { "embed-title": (_a = embed.title) !== null && _a !== void 0 ? _a : undefined, slot: "embeds", "author-image": (_c = (_b = embed.author) === null || _b === void 0 ? void 0 : _b.proxyIconURL) !== null && _c !== void 0 ? _c : (_d = embed.author) === null || _d === void 0 ? void 0 : _d.iconURL, "author-name": (_e = embed.author) === null || _e === void 0 ? void 0 : _e.name, "author-url": (_f = embed.author) === null || _f === void 0 ? void 0 : _f.url, color: (_g = embed.hexColor) !== null && _g !== void 0 ? _g : undefined, image: (_j = (_h = embed.image) === null || _h === void 0 ? void 0 : _h.proxyURL) !== null && _j !== void 0 ? _j : (_k = embed.image) === null || _k === void 0 ? void 0 : _k.url, thumbnail: (_m = (_l = embed.thumbnail) === null || _l === void 0 ? void 0 : _l.proxyURL) !== null && _m !== void 0 ? _m : (_o = embed.thumbnail) === null || _o === void 0 ? void 0 : _o.url, url: (_p = embed.url) !== null && _p !== void 0 ? _p : undefined, children: [embed.description && ((0, jsx_runtime_1.jsx)("discord-embed-description", { slot: "description", children: (0, jsx_runtime_1.jsx)(content_1.default, { content: embed.description, context: Object.assign(Object.assign({}, context), { type: content_1.RenderType.EMBED }) }) })), embed.fields.length > 0 && ((0, jsx_runtime_1.jsx)("discord-embed-fields", { slot: "fields", children: embed.fields.map(async (field, id) => ((0, jsx_runtime_1.jsx)("discord-embed-field", { "field-title": field.name, inline: !!field.inline, "inline-index": field.inline ? (0, embeds_1.calculateInlineIndex)(embed.fields, id) : undefined, children: (0, jsx_runtime_1.jsx)(content_1.default, { content: field.value, context: Object.assign(Object.assign({}, context), { type: content_1.RenderType.EMBED }) }) }, `${context.message.id}-e-${context.index}-f-${id}`))) })), embed.footer && ((0, jsx_runtime_1.jsx)("discord-embed-footer", { slot: "footer", footerImage: (_q = embed.footer.proxyIconURL) !== null && _q !== void 0 ? _q : embed.footer.iconURL, timestamp: (_r = embed.timestamp) !== null && _r !== void 0 ? _r : undefined, children: embed.footer.text }))] }, `${context.message.id}-e-${context.index}`));
14
+ const type = embed.type || 'rich';
15
+ const url = embed.url ? (0, utils_1.safeHref)(embed.url) : undefined;
16
+ const imageUrl = embed.image?.proxyURL ?? embed.image?.url;
17
+ const videoUrl = embed.video?.proxyURL ?? embed.video?.url;
18
+ const provider = embed.provider?.name;
19
+ const safeVid = videoUrl ? (0, utils_1.safeHref)(videoUrl) : null;
20
+ const safeImg = imageUrl ? (0, utils_1.safeHref)(imageUrl) : null;
21
+
22
+ // Image-only embeds (link previews): render the image alone with optional caption
23
+ if (type === 'image' && safeImg) {
24
+ return (0, jsx_runtime_1.jsx)("div", { className: "dht-embed dht-embed--image", children: (0, jsx_runtime_1.jsx)("a", { href: url || safeImg, target: "_blank", rel: "noreferrer", children: (0, jsx_runtime_1.jsx)("img", { src: safeImg, alt: embed.title || '', className: "dht-embed-image", loading: "lazy" }) }) });
25
+ }
26
+ // GIFV — auto-play loop video
27
+ if (type === 'gifv' && safeVid) {
28
+ return (0, jsx_runtime_1.jsx)("div", { className: "dht-embed dht-embed--gifv", children: (0, jsx_runtime_1.jsx)("video", { src: safeVid, autoPlay: true, loop: true, muted: true, playsInline: true, className: "dht-embed-gifv", "aria-label": embed.title || 'GIF' }) });
29
+ }
30
+ // Video embed (YouTube/Vimeo/etc.) — show thumbnail with a play overlay linking to original
31
+ if (type === 'video' && (safeVid || safeImg)) {
32
+ return (0, jsx_runtime_1.jsxs)("div", { className: "dht-embed dht-embed--video", children: [
33
+ (0, jsx_runtime_1.jsxs)("a", { href: url || safeVid || safeImg, target: "_blank", rel: "noreferrer", className: "dht-embed-video-wrap", children: [
34
+ safeImg && (0, jsx_runtime_1.jsx)("img", { src: safeImg, alt: embed.title || '', className: "dht-embed-video-thumb", loading: "lazy" }),
35
+ (0, jsx_runtime_1.jsx)("div", { className: "dht-embed-video-play", children: '▶' })
36
+ ] }),
37
+ provider && (0, jsx_runtime_1.jsx)("div", { className: "dht-embed-provider", children: provider }),
38
+ embed.title && (0, jsx_runtime_1.jsx)("a", { href: url || safeVid, target: "_blank", rel: "noreferrer", className: "dht-embed-title", children: embed.title }),
39
+ ] });
40
+ }
41
+
42
+ // Default rich embed
43
+ const title = embed.title ?? undefined;
44
+ const authorImage = embed.author?.proxyIconURL ?? embed.author?.iconURL;
45
+ const authorName = embed.author?.name;
46
+ const authorUrl = embed.author?.url ? (0, utils_1.safeHref)(embed.author.url) : undefined;
47
+ const color = embed.hexColor ? (0, utils_1.safeColor)(embed.hexColor, undefined) : undefined;
48
+ const thumb = embed.thumbnail?.proxyURL ?? embed.thumbnail?.url;
49
+
50
+ return ((0, jsx_runtime_1.jsxs)("discord-embed", {
51
+ "embed-title": title,
52
+ slot: "embeds",
53
+ "author-image": authorImage,
54
+ "author-name": authorName,
55
+ "author-url": authorUrl,
56
+ color: color,
57
+ image: safeImg,
58
+ thumbnail: thumb,
59
+ url: url,
60
+ children: [
61
+ provider && ((0, jsx_runtime_1.jsx)("div", { className: "dht-embed-provider", slot: "author", children: provider })),
62
+ embed.description && ((0, jsx_runtime_1.jsx)("discord-embed-description", {
63
+ slot: "description",
64
+ children: (0, jsx_runtime_1.jsx)(content_1.default, { content: embed.description, context: Object.assign({}, context, { type: content_1.RenderType.EMBED }) })
65
+ })),
66
+ embed.fields.length > 0 && ((0, jsx_runtime_1.jsx)("discord-embed-fields", {
67
+ slot: "fields",
68
+ children: embed.fields.map((field, id) => ((0, jsx_runtime_1.jsx)(EmbedField, { embed, field, id, context }, `${context.message.id}-e-${context.index}-f-${id}`)))
69
+ })),
70
+ safeVid && ((0, jsx_runtime_1.jsx)("div", { className: "dht-embed-video", children: (0, jsx_runtime_1.jsxs)("a", { href: safeVid, target: "_blank", rel: "noreferrer", children: ['▶ Open video', provider ? ` (${provider})` : ''] }) })),
71
+ embed.footer && ((0, jsx_runtime_1.jsx)("discord-embed-footer", {
72
+ slot: "footer",
73
+ footerImage: embed.footer.proxyIconURL ?? embed.footer.iconURL,
74
+ timestamp: embed.timestamp ?? undefined,
75
+ children: embed.footer.text
76
+ }))
77
+ ]
78
+ }, `${context.message.id}-e-${context.index}`));
79
+ }
80
+
81
+ async function EmbedField({ embed, field, id, context }) {
82
+ return (0, jsx_runtime_1.jsx)("discord-embed-field", {
83
+ "field-title": field.name,
84
+ inline: !!field.inline,
85
+ "inline-index": field.inline ? (0, embeds_1.calculateInlineIndex)(embed.fields, id) : undefined,
86
+ children: (0, jsx_runtime_1.jsx)(content_1.default, { content: field.value, context: Object.assign({}, context, { type: content_1.RenderType.EMBED }) })
87
+ });
43
88
  }
44
- //# sourceMappingURL=embed.js.map
89
+ //# sourceMappingURL=embed.js.map