@copilotkit/channels-discord 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/dist/__tests__/ephemeral.test.d.ts +2 -0
  4. package/dist/__tests__/ephemeral.test.d.ts.map +1 -0
  5. package/dist/__tests__/ephemeral.test.js +26 -0
  6. package/dist/__tests__/intents.test.d.ts +2 -0
  7. package/dist/__tests__/intents.test.d.ts.map +1 -0
  8. package/dist/__tests__/intents.test.js +8 -0
  9. package/dist/__tests__/modal-submit.test.d.ts +2 -0
  10. package/dist/__tests__/modal-submit.test.d.ts.map +1 -0
  11. package/dist/__tests__/modal-submit.test.js +26 -0
  12. package/dist/__tests__/reactions.test.d.ts +2 -0
  13. package/dist/__tests__/reactions.test.d.ts.map +1 -0
  14. package/dist/__tests__/reactions.test.js +87 -0
  15. package/dist/adapter.d.ts +118 -0
  16. package/dist/adapter.d.ts.map +1 -0
  17. package/dist/adapter.js +567 -0
  18. package/dist/adapter.test.d.ts +2 -0
  19. package/dist/adapter.test.d.ts.map +1 -0
  20. package/dist/adapter.test.js +343 -0
  21. package/dist/auto-close-streaming.d.ts +61 -0
  22. package/dist/auto-close-streaming.d.ts.map +1 -0
  23. package/dist/auto-close-streaming.js +276 -0
  24. package/dist/auto-close-streaming.test.d.ts +2 -0
  25. package/dist/auto-close-streaming.test.d.ts.map +1 -0
  26. package/dist/auto-close-streaming.test.js +149 -0
  27. package/dist/built-in-context.d.ts +20 -0
  28. package/dist/built-in-context.d.ts.map +1 -0
  29. package/dist/built-in-context.js +30 -0
  30. package/dist/built-in-context.test.d.ts +2 -0
  31. package/dist/built-in-context.test.d.ts.map +1 -0
  32. package/dist/built-in-context.test.js +21 -0
  33. package/dist/built-in-tools.d.ts +4 -0
  34. package/dist/built-in-tools.d.ts.map +1 -0
  35. package/dist/built-in-tools.js +27 -0
  36. package/dist/built-in-tools.test.d.ts +2 -0
  37. package/dist/built-in-tools.test.d.ts.map +1 -0
  38. package/dist/built-in-tools.test.js +30 -0
  39. package/dist/chunked-message-stream.d.ts +94 -0
  40. package/dist/chunked-message-stream.d.ts.map +1 -0
  41. package/dist/chunked-message-stream.js +203 -0
  42. package/dist/chunked-message-stream.test.d.ts +2 -0
  43. package/dist/chunked-message-stream.test.d.ts.map +1 -0
  44. package/dist/chunked-message-stream.test.js +384 -0
  45. package/dist/commands.d.ts +29 -0
  46. package/dist/commands.d.ts.map +1 -0
  47. package/dist/commands.js +193 -0
  48. package/dist/commands.test.d.ts +2 -0
  49. package/dist/commands.test.d.ts.map +1 -0
  50. package/dist/commands.test.js +281 -0
  51. package/dist/conversation-store.d.ts +45 -0
  52. package/dist/conversation-store.d.ts.map +1 -0
  53. package/dist/conversation-store.js +78 -0
  54. package/dist/conversation-store.test.d.ts +2 -0
  55. package/dist/conversation-store.test.d.ts.map +1 -0
  56. package/dist/conversation-store.test.js +106 -0
  57. package/dist/discord-listener.d.ts +88 -0
  58. package/dist/discord-listener.d.ts.map +1 -0
  59. package/dist/discord-listener.js +118 -0
  60. package/dist/discord-listener.test.d.ts +2 -0
  61. package/dist/discord-listener.test.d.ts.map +1 -0
  62. package/dist/discord-listener.test.js +361 -0
  63. package/dist/download-files.d.ts +94 -0
  64. package/dist/download-files.d.ts.map +1 -0
  65. package/dist/download-files.js +145 -0
  66. package/dist/download-files.test.d.ts +2 -0
  67. package/dist/download-files.test.d.ts.map +1 -0
  68. package/dist/download-files.test.js +181 -0
  69. package/dist/event-renderer.d.ts +36 -0
  70. package/dist/event-renderer.d.ts.map +1 -0
  71. package/dist/event-renderer.js +270 -0
  72. package/dist/event-renderer.test.d.ts +2 -0
  73. package/dist/event-renderer.test.d.ts.map +1 -0
  74. package/dist/event-renderer.test.js +160 -0
  75. package/dist/index.d.ts +24 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +17 -0
  78. package/dist/interaction.d.ts +11 -0
  79. package/dist/interaction.d.ts.map +1 -0
  80. package/dist/interaction.js +138 -0
  81. package/dist/interaction.test.d.ts +2 -0
  82. package/dist/interaction.test.d.ts.map +1 -0
  83. package/dist/interaction.test.js +120 -0
  84. package/dist/markdown.d.ts +7 -0
  85. package/dist/markdown.d.ts.map +1 -0
  86. package/dist/markdown.js +66 -0
  87. package/dist/markdown.test.d.ts +2 -0
  88. package/dist/markdown.test.d.ts.map +1 -0
  89. package/dist/markdown.test.js +47 -0
  90. package/dist/message-stream.d.ts +45 -0
  91. package/dist/message-stream.d.ts.map +1 -0
  92. package/dist/message-stream.js +76 -0
  93. package/dist/message-stream.test.d.ts +2 -0
  94. package/dist/message-stream.test.d.ts.map +1 -0
  95. package/dist/message-stream.test.js +162 -0
  96. package/dist/pending-interactions.d.ts +26 -0
  97. package/dist/pending-interactions.d.ts.map +1 -0
  98. package/dist/pending-interactions.js +50 -0
  99. package/dist/pending-interactions.test.d.ts +2 -0
  100. package/dist/pending-interactions.test.d.ts.map +1 -0
  101. package/dist/pending-interactions.test.js +73 -0
  102. package/dist/render/budget.d.ts +28 -0
  103. package/dist/render/budget.d.ts.map +1 -0
  104. package/dist/render/budget.js +49 -0
  105. package/dist/render/budget.test.d.ts +2 -0
  106. package/dist/render/budget.test.d.ts.map +1 -0
  107. package/dist/render/budget.test.js +59 -0
  108. package/dist/render/components-v2.d.ts +16 -0
  109. package/dist/render/components-v2.d.ts.map +1 -0
  110. package/dist/render/components-v2.js +396 -0
  111. package/dist/render/components-v2.test.d.ts +2 -0
  112. package/dist/render/components-v2.test.d.ts.map +1 -0
  113. package/dist/render/components-v2.test.js +395 -0
  114. package/dist/render/modal.d.ts +12 -0
  115. package/dist/render/modal.d.ts.map +1 -0
  116. package/dist/render/modal.js +44 -0
  117. package/dist/types.d.ts +16 -0
  118. package/dist/types.d.ts.map +1 -0
  119. package/dist/types.js +4 -0
  120. package/dist/types.test.d.ts +2 -0
  121. package/dist/types.test.d.ts.map +1 -0
  122. package/dist/types.test.js +12 -0
  123. package/package.json +56 -0
@@ -0,0 +1,396 @@
1
+ import { ContainerBuilder, TextDisplayBuilder, SeparatorBuilder, SeparatorSpacingSize, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, MediaGalleryBuilder, MediaGalleryItemBuilder, MessageFlags, } from "discord.js";
2
+ import { DISCORD_LIMITS, truncateText, truncateFenced, clampArray, } from "./budget.js";
3
+ import { discordMarkdown } from "../markdown.js";
4
+ const OVERFLOW_TEXT = "_…content truncated_";
5
+ /** Guards one-time warnings so they don't fire on every render. */
6
+ let warnedInputSkipped = false;
7
+ /** True once the message is full; callers must stop adding components. */
8
+ function budgetFull(budget) {
9
+ // Reserve room for one trailing overflow TextDisplay: one component slot plus
10
+ // the marker's text length, so signalOverflow never pushes us over a cap.
11
+ return (budget.components >= DISCORD_LIMITS.componentsPerMessage - 1 ||
12
+ budget.textChars >= DISCORD_LIMITS.totalTextChars - OVERFLOW_TEXT.length);
13
+ }
14
+ /** Append a single trailing overflow marker (idempotent) when the budget is hit. */
15
+ function signalOverflow(budget, container) {
16
+ if (budget.overflowed)
17
+ return;
18
+ budget.overflowed = true;
19
+ budget.components += 1;
20
+ budget.textChars += OVERFLOW_TEXT.length;
21
+ container.addTextDisplayComponents(new TextDisplayBuilder().setContent(OVERFLOW_TEXT));
22
+ }
23
+ /** Add a TextDisplay, charging the text budget and clamping to remaining room. */
24
+ function addText(content, budget, container) {
25
+ // Genuinely-empty INPUT (an empty <Text value="">, an empty <Fields>/<Context>/
26
+ // <Table>) is not an overflow — skip it silently. Only a NON-empty content that
27
+ // the budget clamps to empty signals truncation.
28
+ if (content.length === 0)
29
+ return;
30
+ // Reserve room for one trailing overflow marker (mirrors the component-slot
31
+ // reservation in budgetFull) so the summed text — including a marker that may
32
+ // be appended later — never exceeds totalTextChars.
33
+ const remaining = DISCORD_LIMITS.totalTextChars - OVERFLOW_TEXT.length - budget.textChars;
34
+ let clamped = content;
35
+ if (content.length > remaining) {
36
+ const room = Math.max(0, remaining);
37
+ // The cumulative clamp can sever a ``` fence emitted by the table/section
38
+ // path. If the content carries a fence, use the fence-balancing truncation
39
+ // so a cut fence is re-closed (kept within `room`).
40
+ clamped = content.includes("```")
41
+ ? truncateFenced(content, room)
42
+ : truncateText(content, room);
43
+ }
44
+ // Discord rejects an empty TextDisplay. If the text budget clamped a non-empty
45
+ // content down to empty, emit the overflow marker instead of an empty component.
46
+ if (clamped.length === 0) {
47
+ signalOverflow(budget, container);
48
+ return;
49
+ }
50
+ budget.textChars += clamped.length;
51
+ budget.components += 1;
52
+ container.addTextDisplayComponents(new TextDisplayBuilder().setContent(clamped));
53
+ }
54
+ /**
55
+ * Render a cross-platform IR tree (already expanded + action-bound, so event
56
+ * props are `{ id }`) into a single top-level Components V2 `ContainerBuilder`.
57
+ *
58
+ * Total renderer: unknown intrinsics are skipped, never thrown. Per-element
59
+ * limits apply via truncate/clamp; nothing is silently dropped.
60
+ */
61
+ export function renderComponents(ir) {
62
+ const container = new ContainerBuilder();
63
+ // <Message accent="#hex"> → container accent color.
64
+ if (ir.length === 1 && ir[0]?.type === "message") {
65
+ const accent = ir[0].props.accent;
66
+ const int = parseAccent(accent);
67
+ if (int !== undefined)
68
+ container.setAccentColor(int);
69
+ }
70
+ const budget = {
71
+ components: 0,
72
+ textChars: 0,
73
+ overflowed: false,
74
+ };
75
+ for (const node of ir)
76
+ addNode(node, container, budget);
77
+ return container;
78
+ }
79
+ /** Ready-to-send payload for channel.send / message.edit. */
80
+ export function renderDiscordMessage(ir) {
81
+ return {
82
+ components: [renderComponents(ir)],
83
+ flags: MessageFlags.IsComponentsV2,
84
+ };
85
+ }
86
+ function addNode(node, container, budget) {
87
+ if (typeof node.type !== "string")
88
+ return; // non-intrinsic — already expanded
89
+ // <Message> is a structural wrapper; recurse into it without charging budget.
90
+ if (node.type === "message") {
91
+ for (const child of childNodes(node))
92
+ addNode(child, container, budget);
93
+ return;
94
+ }
95
+ // Message-level budget reached — emit one overflow marker and stop adding.
96
+ if (budgetFull(budget)) {
97
+ signalOverflow(budget, container);
98
+ return;
99
+ }
100
+ const props = node.props ?? {};
101
+ switch (node.type) {
102
+ case "header": {
103
+ addText("# " + truncateText(collectText(node), DISCORD_LIMITS.headerText), budget, container);
104
+ return;
105
+ }
106
+ case "section":
107
+ case "markdown":
108
+ case "text": {
109
+ const raw = node.type === "text" ? String(props.value ?? "") : collectText(node);
110
+ // Fence-safe: truncate the rendered (possibly fenced) markdown without
111
+ // cutting a closing ``` open.
112
+ addText(truncateFenced(discordMarkdown(raw), DISCORD_LIMITS.textDisplayChars), budget, container);
113
+ return;
114
+ }
115
+ case "fields": {
116
+ // No native field grid in CV2 — render each field as a bold-label line.
117
+ const fields = childNodes(node).filter((c) => c.type === "field");
118
+ const lines = fields.map((f) => `**${collectFieldLabel(f)}** ${collectFieldValue(f)}`.trim());
119
+ addText(truncateFenced(discordMarkdown(lines.join("\n")), DISCORD_LIMITS.textDisplayChars), budget, container);
120
+ return;
121
+ }
122
+ case "field": {
123
+ addText(truncateFenced(discordMarkdown(collectText(node)), DISCORD_LIMITS.textDisplayChars), budget, container);
124
+ return;
125
+ }
126
+ case "context": {
127
+ // Discord subtext: lines prefixed with `-# `.
128
+ const parts = childNodes(node)
129
+ .map((c) => collectText(c))
130
+ .filter(Boolean);
131
+ const body = parts.map((p) => `-# ${p}`).join("\n");
132
+ addText(truncateText(body, DISCORD_LIMITS.textDisplayChars), budget, container);
133
+ return;
134
+ }
135
+ case "divider": {
136
+ budget.components += 1;
137
+ container.addSeparatorComponents(new SeparatorBuilder()
138
+ .setDivider(true)
139
+ .setSpacing(SeparatorSpacingSize.Small));
140
+ return;
141
+ }
142
+ case "image": {
143
+ const url = (props.url ?? props.image_url);
144
+ if (url) {
145
+ // Discord counts the MediaGallery PLUS every nested item toward the cap.
146
+ // One image node = the gallery (1) + a single item (1).
147
+ const cost = 1 + 1;
148
+ if (budget.components + cost >
149
+ DISCORD_LIMITS.componentsPerMessage - 1) {
150
+ signalOverflow(budget, container);
151
+ return;
152
+ }
153
+ budget.components += cost;
154
+ container.addMediaGalleryComponents(new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder().setURL(url)));
155
+ }
156
+ return;
157
+ }
158
+ case "actions": {
159
+ for (const row of buildActionRows(childNodes(node))) {
160
+ // Discord counts the ActionRow PLUS every nested button/select toward the
161
+ // componentsPerMessage cap. Charge the full nested cost and check the
162
+ // projected total (reserving one slot for the overflow marker) before adding.
163
+ const cost = 1 + row.components.length;
164
+ if (budget.components + cost >
165
+ DISCORD_LIMITS.componentsPerMessage - 1) {
166
+ signalOverflow(budget, container);
167
+ break;
168
+ }
169
+ budget.components += cost;
170
+ container.addActionRowComponents(row);
171
+ }
172
+ return;
173
+ }
174
+ case "table": {
175
+ // Discord has no tables; emit a fenced text block via discordMarkdown over
176
+ // a reconstructed pipe table. Fence-safe truncation keeps the closing ```.
177
+ const md = tableToMarkdown(node);
178
+ addText(truncateFenced(discordMarkdown(md), DISCORD_LIMITS.textDisplayChars), budget, container);
179
+ return;
180
+ }
181
+ case "input": {
182
+ // Free-standing text inputs are modal-only on Discord; modals are deferred
183
+ // to a follow-up. Log once and skip (total renderer).
184
+ if (!warnedInputSkipped) {
185
+ warnedInputSkipped = true;
186
+ console.warn("[bot-discord] <Input> is modal-only; skipped (modals not in v1).");
187
+ }
188
+ return;
189
+ }
190
+ default:
191
+ return; // unknown intrinsic — skip
192
+ }
193
+ }
194
+ /** Build action rows from a flat list of button/select children, chunking buttons ≤5/row. */
195
+ function buildActionRows(children) {
196
+ const rows = [];
197
+ let current = [];
198
+ const flushButtons = () => {
199
+ if (current.length === 0)
200
+ return;
201
+ rows.push(new ActionRowBuilder().addComponents(...current));
202
+ current = [];
203
+ };
204
+ for (const child of children) {
205
+ if (rows.length >= DISCORD_LIMITS.actionRows)
206
+ break;
207
+ if (child.type === "button") {
208
+ const btn = buildButton(child);
209
+ if (!btn)
210
+ continue;
211
+ current.push(btn);
212
+ if (current.length === DISCORD_LIMITS.buttonsPerRow)
213
+ flushButtons();
214
+ }
215
+ else if (child.type === "select") {
216
+ flushButtons();
217
+ const select = buildSelect(child);
218
+ if (select) {
219
+ rows.push(new ActionRowBuilder().addComponents(select));
220
+ }
221
+ }
222
+ }
223
+ flushButtons();
224
+ return rows.slice(0, DISCORD_LIMITS.actionRows);
225
+ }
226
+ function buildButton(node) {
227
+ const props = node.props ?? {};
228
+ const label = truncateText(collectText(node) || " ", DISCORD_LIMITS.buttonLabel);
229
+ // Link button: opens a URL natively, carries no custom_id, never dispatches.
230
+ if (typeof props.url === "string" && props.url.length > 0) {
231
+ return new ButtonBuilder()
232
+ .setStyle(ButtonStyle.Link)
233
+ .setLabel(label)
234
+ .setURL(props.url);
235
+ }
236
+ const id = buttonCustomId(idFromHandler(props.onClick), props.value);
237
+ if (!id)
238
+ return undefined;
239
+ const btn = new ButtonBuilder()
240
+ .setCustomId(truncateText(id, DISCORD_LIMITS.customId))
241
+ .setLabel(label)
242
+ .setStyle(buttonStyle(props.style));
243
+ return btn;
244
+ }
245
+ function buildSelect(node) {
246
+ const props = node.props ?? {};
247
+ const id = idFromHandler(props.onSelect);
248
+ if (!id)
249
+ return undefined;
250
+ const options = props.options ?? [];
251
+ // Clamp to Discord's 25-option cap. A fake "+N more…" indicator is NOT an
252
+ // option here: it would be a selectable garbage value that dispatches as a
253
+ // real selection. Drop the overflow and warn instead (clamp, never a bogus
254
+ // selectable value).
255
+ const { items, overflow } = clampArray(options, DISCORD_LIMITS.selectOptions);
256
+ if (overflow > 0) {
257
+ console.warn(`[bot-discord] <Select> has more than ${DISCORD_LIMITS.selectOptions} options; dropping ${overflow}.`);
258
+ }
259
+ const built = items.map((o) => new StringSelectMenuOptionBuilder()
260
+ .setLabel(truncateText(o.label, 100))
261
+ .setValue(truncateText(String(o.value), 100)));
262
+ // Discord rejects a StringSelectMenu with zero options (needs 1..25). With no
263
+ // options there's nothing selectable, so omit the select entirely (the caller
264
+ // already filters undefined, mirroring the button path).
265
+ if (built.length === 0)
266
+ return undefined;
267
+ const select = new StringSelectMenuBuilder()
268
+ .setCustomId(truncateText(id, DISCORD_LIMITS.customId))
269
+ // Truthy fallback so an explicit "" placeholder falls back to " " (Discord
270
+ // rejects an empty placeholder), matching the button-label path.
271
+ .setPlaceholder(truncateText(String(props.placeholder || " "), DISCORD_LIMITS.selectPlaceholder))
272
+ .addOptions(built);
273
+ // Multi-select: allow 0..N picks. maxValues > 1 is also the signal the decoder
274
+ // reads (interaction.component.maxValues) to return a string[] instead of one.
275
+ if (props.multi) {
276
+ select.setMinValues(0).setMaxValues(built.length);
277
+ }
278
+ return select;
279
+ }
280
+ function buttonStyle(style) {
281
+ if (style === "primary")
282
+ return ButtonStyle.Primary;
283
+ if (style === "danger")
284
+ return ButtonStyle.Danger;
285
+ return ButtonStyle.Secondary;
286
+ }
287
+ /** Extract `{ id }` the action registry stamped onto an event prop. */
288
+ function idFromHandler(handler) {
289
+ if (handler && typeof handler === "object" && "id" in handler) {
290
+ const id = handler.id;
291
+ if (typeof id === "string")
292
+ return id;
293
+ }
294
+ return undefined;
295
+ }
296
+ /**
297
+ * A button with no handler but a `value` still needs a stable custom_id. The
298
+ * value is encoded as `v:<json>` so {@link unpackValue} can recover it. If that
299
+ * encoding would exceed the custom_id cap we must NOT truncate — a truncated
300
+ * JSON string is corrupt and silently decodes to undefined. Instead omit the
301
+ * value entirely (caller falls back to the handler id) and warn.
302
+ */
303
+ function packValueId(value) {
304
+ if (value === undefined)
305
+ return undefined;
306
+ const id = "v:" + JSON.stringify(value);
307
+ if (id.length > DISCORD_LIMITS.customId) {
308
+ console.warn("[bot-discord] button `value` too large to encode in a custom_id; dropping the bound value.");
309
+ return undefined;
310
+ }
311
+ return id;
312
+ }
313
+ /**
314
+ * A button can have BOTH an `onClick` (which updates the card in place) AND a
315
+ * `value` (which resolves an `awaitChoice` waiter) — e.g. the HITL confirm
316
+ * gate. Discord exposes only a single `custom_id` per button (unlike Slack's
317
+ * separate `action_id` + `value`), so we pack both into it as
318
+ * `<handlerId>;v:<json>`; {@link decodeInteraction} splits them back into a
319
+ * dispatchable id and the bound value. Without this the value is dropped and
320
+ * `awaitChoice` resolves to `undefined` (every approval reads as "declined").
321
+ */
322
+ function buttonCustomId(handlerId, value) {
323
+ const valueId = packValueId(value);
324
+ if (handlerId && valueId) {
325
+ const combined = `${handlerId};${valueId}`;
326
+ if (combined.length <= DISCORD_LIMITS.customId)
327
+ return combined;
328
+ console.warn("[bot-discord] button onClick+value custom_id exceeds the 100-char cap; keeping the handler, dropping the bound value.");
329
+ return handlerId;
330
+ }
331
+ return handlerId ?? valueId;
332
+ }
333
+ const HEX6 = /^#?[0-9a-fA-F]{6}$/;
334
+ /**
335
+ * "#5865F2" / "5865F2" / 0x5865F2 → integer; undefined if unparseable.
336
+ * Strict: a string must be exactly 6 hex digits (optionally `#`-prefixed); a
337
+ * number must fall within the 0..0xFFFFFF RGB range.
338
+ */
339
+ function parseAccent(accent) {
340
+ if (typeof accent === "number") {
341
+ if (!Number.isInteger(accent) || accent < 0 || accent > 0xffffff)
342
+ return undefined;
343
+ return accent;
344
+ }
345
+ if (typeof accent !== "string" || !HEX6.test(accent))
346
+ return undefined;
347
+ return Number.parseInt(accent.replace(/^#/, ""), 16);
348
+ }
349
+ // ── helpers copied verbatim from bot-slack/src/render/block-kit.ts ──
350
+ function childNodes(node) {
351
+ const children = node.props?.children;
352
+ if (Array.isArray(children))
353
+ return children;
354
+ if (children &&
355
+ typeof children === "object" &&
356
+ "type" in children) {
357
+ return [children];
358
+ }
359
+ return [];
360
+ }
361
+ function collectText(node) {
362
+ if (typeof node.type === "string" && node.type === "text") {
363
+ return String(node.props?.value ?? "");
364
+ }
365
+ let acc = "";
366
+ for (const child of childNodes(node)) {
367
+ acc += collectText(child);
368
+ }
369
+ return acc;
370
+ }
371
+ // Field label/value: a <Field label="..">value</Field> or text-only field.
372
+ function collectFieldLabel(node) {
373
+ const label = node.props?.label;
374
+ return typeof label === "string" ? label : "";
375
+ }
376
+ function collectFieldValue(node) {
377
+ return collectText(node);
378
+ }
379
+ // Reconstruct a GFM pipe table from a <Table columns rows> node so
380
+ // discordMarkdown can fence it. Columns from props.columns; rows from row/cell.
381
+ function tableToMarkdown(node) {
382
+ const columns = node.props?.columns?.map((c) => c.header) ?? [];
383
+ const rows = childNodes(node)
384
+ .filter((c) => c.type === "row")
385
+ .map((r) => childNodes(r)
386
+ .filter((c) => c.type === "cell")
387
+ .map((c) => collectText(c)));
388
+ const lines = [];
389
+ if (columns.length) {
390
+ lines.push(`| ${columns.join(" | ")} |`);
391
+ lines.push(`| ${columns.map(() => "-").join(" | ")} |`);
392
+ }
393
+ for (const row of rows)
394
+ lines.push(`| ${row.join(" | ")} |`);
395
+ return lines.join("\n");
396
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=components-v2.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components-v2.test.d.ts","sourceRoot":"","sources":["../../src/render/components-v2.test.ts"],"names":[],"mappings":""}