kimaki 0.4.21 → 0.4.22
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/cli.js +1 -1
- package/dist/discordBot.js +60 -31
- package/dist/format-tables.js +93 -0
- package/dist/format-tables.test.js +418 -0
- package/dist/markdown.js +3 -3
- package/dist/tools.js +2 -4
- package/dist/utils.js +31 -0
- package/package.json +1 -2
- package/src/cli.ts +1 -1
- package/src/discordBot.ts +64 -36
- package/src/format-tables.test.ts +440 -0
- package/src/format-tables.ts +106 -0
- package/src/markdown.ts +3 -3
- package/src/tools.ts +2 -4
- package/src/utils.ts +37 -0
package/src/discordBot.ts
CHANGED
|
@@ -46,6 +46,7 @@ import * as prism from 'prism-media'
|
|
|
46
46
|
import dedent from 'string-dedent'
|
|
47
47
|
import { transcribeAudio } from './voice.js'
|
|
48
48
|
import { extractTagsArrays, extractNonXmlContent } from './xml.js'
|
|
49
|
+
import { formatMarkdownTables } from './format-tables.js'
|
|
49
50
|
import prettyMilliseconds from 'pretty-ms'
|
|
50
51
|
import type { Session } from '@google/genai'
|
|
51
52
|
import { createLogger } from './logger.js'
|
|
@@ -756,6 +757,7 @@ async function sendThreadMessage(
|
|
|
756
757
|
): Promise<Message> {
|
|
757
758
|
const MAX_LENGTH = 2000
|
|
758
759
|
|
|
760
|
+
content = formatMarkdownTables(content)
|
|
759
761
|
content = escapeBackticksInCodeBlocks(content)
|
|
760
762
|
|
|
761
763
|
const chunks = splitMarkdownForDiscord({ content, maxLength: MAX_LENGTH })
|
|
@@ -1092,9 +1094,9 @@ function escapeInlineCode(text: string): string {
|
|
|
1092
1094
|
.replace(/\|\|/g, '\\|\\|') // Double pipes (spoiler syntax)
|
|
1093
1095
|
}
|
|
1094
1096
|
|
|
1095
|
-
function resolveTextChannel(
|
|
1097
|
+
async function resolveTextChannel(
|
|
1096
1098
|
channel: TextChannel | ThreadChannel | null | undefined,
|
|
1097
|
-
): TextChannel | null {
|
|
1099
|
+
): Promise<TextChannel | null> {
|
|
1098
1100
|
if (!channel) {
|
|
1099
1101
|
return null
|
|
1100
1102
|
}
|
|
@@ -1108,9 +1110,12 @@ function resolveTextChannel(
|
|
|
1108
1110
|
channel.type === ChannelType.PrivateThread ||
|
|
1109
1111
|
channel.type === ChannelType.AnnouncementThread
|
|
1110
1112
|
) {
|
|
1111
|
-
const
|
|
1112
|
-
if (
|
|
1113
|
-
|
|
1113
|
+
const parentId = channel.parentId
|
|
1114
|
+
if (parentId) {
|
|
1115
|
+
const parent = await channel.guild.channels.fetch(parentId)
|
|
1116
|
+
if (parent?.type === ChannelType.GuildText) {
|
|
1117
|
+
return parent as TextChannel
|
|
1118
|
+
}
|
|
1114
1119
|
}
|
|
1115
1120
|
}
|
|
1116
1121
|
|
|
@@ -1328,10 +1333,20 @@ function getToolSummaryText(part: Part): string {
|
|
|
1328
1333
|
return pattern ? `*${pattern}*` : ''
|
|
1329
1334
|
}
|
|
1330
1335
|
|
|
1331
|
-
if (part.tool === 'bash' || part.tool === '
|
|
1336
|
+
if (part.tool === 'bash' || part.tool === 'todoread' || part.tool === 'todowrite') {
|
|
1332
1337
|
return ''
|
|
1333
1338
|
}
|
|
1334
1339
|
|
|
1340
|
+
if (part.tool === 'task') {
|
|
1341
|
+
const description = (part.state.input?.description as string) || ''
|
|
1342
|
+
return description ? `_${description}_` : ''
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (part.tool === 'skill') {
|
|
1346
|
+
const name = (part.state.input?.name as string) || ''
|
|
1347
|
+
return name ? `_${name}_` : ''
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1335
1350
|
if (!part.state.input) return ''
|
|
1336
1351
|
|
|
1337
1352
|
const inputFields = Object.entries(part.state.input)
|
|
@@ -1355,19 +1370,12 @@ function formatTodoList(part: Part): string {
|
|
|
1355
1370
|
content: string
|
|
1356
1371
|
status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
|
|
1357
1372
|
}[]) || []
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
}
|
|
1365
|
-
if (todo.status === 'completed' || todo.status === 'cancelled') {
|
|
1366
|
-
return `${num} ~~${todo.content}~~`
|
|
1367
|
-
}
|
|
1368
|
-
return `${num} ${todo.content}`
|
|
1369
|
-
})
|
|
1370
|
-
.join('\n')
|
|
1373
|
+
const activeIndex = todos.findIndex((todo) => {
|
|
1374
|
+
return todo.status === 'in_progress'
|
|
1375
|
+
})
|
|
1376
|
+
const activeTodo = todos[activeIndex]
|
|
1377
|
+
if (activeIndex === -1 || !activeTodo) return ''
|
|
1378
|
+
return `${activeIndex + 1}. **${activeTodo.content}**`
|
|
1371
1379
|
}
|
|
1372
1380
|
|
|
1373
1381
|
function formatPart(part: Part): string {
|
|
@@ -1415,9 +1423,9 @@ function formatPart(part: Part): string {
|
|
|
1415
1423
|
const command = (part.state.input?.command as string) || ''
|
|
1416
1424
|
const description = (part.state.input?.description as string) || ''
|
|
1417
1425
|
const isSingleLine = !command.includes('\n')
|
|
1418
|
-
const
|
|
1419
|
-
if (isSingleLine && !
|
|
1420
|
-
toolTitle =
|
|
1426
|
+
const hasUnderscores = command.includes('_')
|
|
1427
|
+
if (isSingleLine && !hasUnderscores && command.length <= 50) {
|
|
1428
|
+
toolTitle = `_${command}_`
|
|
1421
1429
|
} else if (description) {
|
|
1422
1430
|
toolTitle = `_${description}_`
|
|
1423
1431
|
} else if (stateTitle) {
|
|
@@ -1581,6 +1589,8 @@ async function handleOpencodeSession({
|
|
|
1581
1589
|
let usedModel: string | undefined
|
|
1582
1590
|
let usedProviderID: string | undefined
|
|
1583
1591
|
let tokensUsedInSession = 0
|
|
1592
|
+
let lastDisplayedContextPercentage = 0
|
|
1593
|
+
let modelContextLimit: number | undefined
|
|
1584
1594
|
|
|
1585
1595
|
let typingInterval: NodeJS.Timeout | null = null
|
|
1586
1596
|
|
|
@@ -1676,6 +1686,30 @@ async function handleOpencodeSession({
|
|
|
1676
1686
|
assistantMessageId = msg.id
|
|
1677
1687
|
usedModel = msg.modelID
|
|
1678
1688
|
usedProviderID = msg.providerID
|
|
1689
|
+
|
|
1690
|
+
if (tokensUsedInSession > 0 && usedProviderID && usedModel) {
|
|
1691
|
+
if (!modelContextLimit) {
|
|
1692
|
+
try {
|
|
1693
|
+
const providersResponse = await getClient().provider.list({ query: { directory } })
|
|
1694
|
+
const provider = providersResponse.data?.all?.find((p) => p.id === usedProviderID)
|
|
1695
|
+
const model = provider?.models?.[usedModel]
|
|
1696
|
+
if (model?.limit?.context) {
|
|
1697
|
+
modelContextLimit = model.limit.context
|
|
1698
|
+
}
|
|
1699
|
+
} catch (e) {
|
|
1700
|
+
sessionLogger.error('Failed to fetch provider info for context limit:', e)
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
if (modelContextLimit) {
|
|
1705
|
+
const currentPercentage = Math.floor((tokensUsedInSession / modelContextLimit) * 100)
|
|
1706
|
+
const thresholdCrossed = Math.floor(currentPercentage / 10) * 10
|
|
1707
|
+
if (thresholdCrossed > lastDisplayedContextPercentage && thresholdCrossed >= 10) {
|
|
1708
|
+
lastDisplayedContextPercentage = thresholdCrossed
|
|
1709
|
+
await sendThreadMessage(thread, `◼︎ context usage ${currentPercentage}%`)
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1679
1713
|
}
|
|
1680
1714
|
} else if (event.type === 'message.part.updated') {
|
|
1681
1715
|
const part = event.properties.part
|
|
@@ -2328,11 +2362,8 @@ export async function startDiscordBot({
|
|
|
2328
2362
|
|
|
2329
2363
|
// Get the channel's project directory from its topic
|
|
2330
2364
|
let projectDirectory: string | undefined
|
|
2331
|
-
if (
|
|
2332
|
-
|
|
2333
|
-
interaction.channel.type === ChannelType.GuildText
|
|
2334
|
-
) {
|
|
2335
|
-
const textChannel = resolveTextChannel(
|
|
2365
|
+
if (interaction.channel) {
|
|
2366
|
+
const textChannel = await resolveTextChannel(
|
|
2336
2367
|
interaction.channel as TextChannel | ThreadChannel | null,
|
|
2337
2368
|
)
|
|
2338
2369
|
if (textChannel) {
|
|
@@ -2414,11 +2445,8 @@ export async function startDiscordBot({
|
|
|
2414
2445
|
|
|
2415
2446
|
// Get the channel's project directory from its topic
|
|
2416
2447
|
let projectDirectory: string | undefined
|
|
2417
|
-
if (
|
|
2418
|
-
|
|
2419
|
-
interaction.channel.type === ChannelType.GuildText
|
|
2420
|
-
) {
|
|
2421
|
-
const textChannel = resolveTextChannel(
|
|
2448
|
+
if (interaction.channel) {
|
|
2449
|
+
const textChannel = await resolveTextChannel(
|
|
2422
2450
|
interaction.channel as TextChannel | ThreadChannel | null,
|
|
2423
2451
|
)
|
|
2424
2452
|
if (textChannel) {
|
|
@@ -2782,7 +2810,7 @@ export async function startDiscordBot({
|
|
|
2782
2810
|
if (partsToRender.length > 0) {
|
|
2783
2811
|
const combinedContent = partsToRender
|
|
2784
2812
|
.map((p) => p.content)
|
|
2785
|
-
.join('\n
|
|
2813
|
+
.join('\n')
|
|
2786
2814
|
|
|
2787
2815
|
const discordMessage = await sendThreadMessage(
|
|
2788
2816
|
thread,
|
|
@@ -2887,7 +2915,7 @@ export async function startDiscordBot({
|
|
|
2887
2915
|
`Failed to create channels: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2888
2916
|
)
|
|
2889
2917
|
}
|
|
2890
|
-
} else if (command.commandName === '
|
|
2918
|
+
} else if (command.commandName === 'create-new-project') {
|
|
2891
2919
|
await command.deferReply({ ephemeral: false })
|
|
2892
2920
|
|
|
2893
2921
|
const projectName = command.options.getString('name', true)
|
|
@@ -3122,7 +3150,7 @@ export async function startDiscordBot({
|
|
|
3122
3150
|
return
|
|
3123
3151
|
}
|
|
3124
3152
|
|
|
3125
|
-
const textChannel = resolveTextChannel(channel as ThreadChannel)
|
|
3153
|
+
const textChannel = await resolveTextChannel(channel as ThreadChannel)
|
|
3126
3154
|
const { projectDirectory: directory } = getKimakiMetadata(textChannel)
|
|
3127
3155
|
|
|
3128
3156
|
if (!directory) {
|
|
@@ -3193,7 +3221,7 @@ export async function startDiscordBot({
|
|
|
3193
3221
|
return
|
|
3194
3222
|
}
|
|
3195
3223
|
|
|
3196
|
-
const textChannel = resolveTextChannel(channel as ThreadChannel)
|
|
3224
|
+
const textChannel = await resolveTextChannel(channel as ThreadChannel)
|
|
3197
3225
|
const { projectDirectory: directory } = getKimakiMetadata(textChannel)
|
|
3198
3226
|
|
|
3199
3227
|
if (!directory) {
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { test, expect } from 'vitest'
|
|
2
|
+
import { formatMarkdownTables } from './format-tables.js'
|
|
3
|
+
|
|
4
|
+
test('formats simple table', () => {
|
|
5
|
+
const input = `| Name | Age |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Alice | 30 |
|
|
8
|
+
| Bob | 25 |`
|
|
9
|
+
const result = formatMarkdownTables(input)
|
|
10
|
+
expect(result).toMatchInlineSnapshot(`
|
|
11
|
+
"\`\`\`
|
|
12
|
+
Name Age
|
|
13
|
+
----- ---
|
|
14
|
+
Alice 30
|
|
15
|
+
Bob 25
|
|
16
|
+
\`\`\`
|
|
17
|
+
"
|
|
18
|
+
`)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('formats table with varying column widths', () => {
|
|
22
|
+
const input = `| Item | Quantity | Price |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| Apples | 10 | $5 |
|
|
25
|
+
| Oranges | 3 | $2 |
|
|
26
|
+
| Bananas with long name | 100 | $15.99 |`
|
|
27
|
+
const result = formatMarkdownTables(input)
|
|
28
|
+
expect(result).toMatchInlineSnapshot(`
|
|
29
|
+
"\`\`\`
|
|
30
|
+
Item Quantity Price
|
|
31
|
+
---------------------- -------- ------
|
|
32
|
+
Apples 10 $5
|
|
33
|
+
Oranges 3 $2
|
|
34
|
+
Bananas with long name 100 $15.99
|
|
35
|
+
\`\`\`
|
|
36
|
+
"
|
|
37
|
+
`)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('strips bold formatting from cells', () => {
|
|
41
|
+
const input = `| Header | Value |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| **Bold text** | Normal |
|
|
44
|
+
| Mixed **bold** text | Another |`
|
|
45
|
+
const result = formatMarkdownTables(input)
|
|
46
|
+
expect(result).toMatchInlineSnapshot(`
|
|
47
|
+
"\`\`\`
|
|
48
|
+
Header Value
|
|
49
|
+
--------------- -------
|
|
50
|
+
Bold text Normal
|
|
51
|
+
Mixed bold text Another
|
|
52
|
+
\`\`\`
|
|
53
|
+
"
|
|
54
|
+
`)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('strips italic formatting from cells', () => {
|
|
58
|
+
const input = `| Header | Value |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| *Italic text* | Normal |
|
|
61
|
+
| _Also italic_ | Another |`
|
|
62
|
+
const result = formatMarkdownTables(input)
|
|
63
|
+
expect(result).toMatchInlineSnapshot(`
|
|
64
|
+
"\`\`\`
|
|
65
|
+
Header Value
|
|
66
|
+
----------- -------
|
|
67
|
+
Italic text Normal
|
|
68
|
+
Also italic Another
|
|
69
|
+
\`\`\`
|
|
70
|
+
"
|
|
71
|
+
`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('extracts URL from links', () => {
|
|
75
|
+
const input = `| Name | Link |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| Google | [Click here](https://google.com) |
|
|
78
|
+
| GitHub | [GitHub Home](https://github.com) |`
|
|
79
|
+
const result = formatMarkdownTables(input)
|
|
80
|
+
expect(result).toMatchInlineSnapshot(`
|
|
81
|
+
"\`\`\`
|
|
82
|
+
Name Link
|
|
83
|
+
------ ------------------
|
|
84
|
+
Google https://google.com
|
|
85
|
+
GitHub https://github.com
|
|
86
|
+
\`\`\`
|
|
87
|
+
"
|
|
88
|
+
`)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('handles inline code in cells', () => {
|
|
92
|
+
const input = `| Function | Description |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| \`console.log\` | Logs to console |
|
|
95
|
+
| \`Array.map\` | Maps array items |`
|
|
96
|
+
const result = formatMarkdownTables(input)
|
|
97
|
+
expect(result).toMatchInlineSnapshot(`
|
|
98
|
+
"\`\`\`
|
|
99
|
+
Function Description
|
|
100
|
+
----------- ----------------
|
|
101
|
+
console.log Logs to console
|
|
102
|
+
Array.map Maps array items
|
|
103
|
+
\`\`\`
|
|
104
|
+
"
|
|
105
|
+
`)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('handles mixed formatting in single cell', () => {
|
|
109
|
+
const input = `| Description |
|
|
110
|
+
| --- |
|
|
111
|
+
| This has **bold**, *italic*, and \`code\` |
|
|
112
|
+
| Also [a link](https://example.com) here |`
|
|
113
|
+
const result = formatMarkdownTables(input)
|
|
114
|
+
expect(result).toMatchInlineSnapshot(`
|
|
115
|
+
"\`\`\`
|
|
116
|
+
Description
|
|
117
|
+
-------------------------------
|
|
118
|
+
This has bold, italic, and code
|
|
119
|
+
Also https://example.com here
|
|
120
|
+
\`\`\`
|
|
121
|
+
"
|
|
122
|
+
`)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('handles strikethrough text', () => {
|
|
126
|
+
const input = `| Status | Item |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| Done | ~~Deleted item~~ |
|
|
129
|
+
| Active | Normal item |`
|
|
130
|
+
const result = formatMarkdownTables(input)
|
|
131
|
+
expect(result).toMatchInlineSnapshot(`
|
|
132
|
+
"\`\`\`
|
|
133
|
+
Status Item
|
|
134
|
+
------ ------------
|
|
135
|
+
Done Deleted item
|
|
136
|
+
Active Normal item
|
|
137
|
+
\`\`\`
|
|
138
|
+
"
|
|
139
|
+
`)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('preserves content before table', () => {
|
|
143
|
+
const input = `Here is some text before the table.
|
|
144
|
+
|
|
145
|
+
| Col A | Col B |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| 1 | 2 |`
|
|
148
|
+
const result = formatMarkdownTables(input)
|
|
149
|
+
expect(result).toMatchInlineSnapshot(`
|
|
150
|
+
"Here is some text before the table.
|
|
151
|
+
|
|
152
|
+
\`\`\`
|
|
153
|
+
Col A Col B
|
|
154
|
+
----- -----
|
|
155
|
+
1 2
|
|
156
|
+
\`\`\`
|
|
157
|
+
"
|
|
158
|
+
`)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('preserves content after table', () => {
|
|
162
|
+
const input = `| Col A | Col B |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| 1 | 2 |
|
|
165
|
+
|
|
166
|
+
And here is text after.`
|
|
167
|
+
const result = formatMarkdownTables(input)
|
|
168
|
+
expect(result).toMatchInlineSnapshot(`
|
|
169
|
+
"\`\`\`
|
|
170
|
+
Col A Col B
|
|
171
|
+
----- -----
|
|
172
|
+
1 2
|
|
173
|
+
\`\`\`
|
|
174
|
+
And here is text after."
|
|
175
|
+
`)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('preserves content before and after table', () => {
|
|
179
|
+
const input = `Some intro text.
|
|
180
|
+
|
|
181
|
+
| Name | Value |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| Key | 123 |
|
|
184
|
+
|
|
185
|
+
Some outro text.`
|
|
186
|
+
const result = formatMarkdownTables(input)
|
|
187
|
+
expect(result).toMatchInlineSnapshot(`
|
|
188
|
+
"Some intro text.
|
|
189
|
+
|
|
190
|
+
\`\`\`
|
|
191
|
+
Name Value
|
|
192
|
+
---- -----
|
|
193
|
+
Key 123
|
|
194
|
+
\`\`\`
|
|
195
|
+
Some outro text."
|
|
196
|
+
`)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('handles multiple tables in same content', () => {
|
|
200
|
+
const input = `First table:
|
|
201
|
+
|
|
202
|
+
| A | B |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| 1 | 2 |
|
|
205
|
+
|
|
206
|
+
Some text between.
|
|
207
|
+
|
|
208
|
+
Second table:
|
|
209
|
+
|
|
210
|
+
| X | Y | Z |
|
|
211
|
+
| --- | --- | --- |
|
|
212
|
+
| a | b | c |`
|
|
213
|
+
const result = formatMarkdownTables(input)
|
|
214
|
+
expect(result).toMatchInlineSnapshot(`
|
|
215
|
+
"First table:
|
|
216
|
+
|
|
217
|
+
\`\`\`
|
|
218
|
+
A B
|
|
219
|
+
- -
|
|
220
|
+
1 2
|
|
221
|
+
\`\`\`
|
|
222
|
+
Some text between.
|
|
223
|
+
|
|
224
|
+
Second table:
|
|
225
|
+
|
|
226
|
+
\`\`\`
|
|
227
|
+
X Y Z
|
|
228
|
+
- - -
|
|
229
|
+
a b c
|
|
230
|
+
\`\`\`
|
|
231
|
+
"
|
|
232
|
+
`)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('handles empty cells', () => {
|
|
236
|
+
const input = `| Name | Optional |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| Alice | |
|
|
239
|
+
| | Bob |
|
|
240
|
+
| | |`
|
|
241
|
+
const result = formatMarkdownTables(input)
|
|
242
|
+
expect(result).toMatchInlineSnapshot(`
|
|
243
|
+
"\`\`\`
|
|
244
|
+
Name Optional
|
|
245
|
+
----- --------
|
|
246
|
+
Alice
|
|
247
|
+
Bob
|
|
248
|
+
|
|
249
|
+
\`\`\`
|
|
250
|
+
"
|
|
251
|
+
`)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('handles single column table', () => {
|
|
255
|
+
const input = `| Items |
|
|
256
|
+
| --- |
|
|
257
|
+
| Apple |
|
|
258
|
+
| Banana |
|
|
259
|
+
| Cherry |`
|
|
260
|
+
const result = formatMarkdownTables(input)
|
|
261
|
+
expect(result).toMatchInlineSnapshot(`
|
|
262
|
+
"\`\`\`
|
|
263
|
+
Items
|
|
264
|
+
------
|
|
265
|
+
Apple
|
|
266
|
+
Banana
|
|
267
|
+
Cherry
|
|
268
|
+
\`\`\`
|
|
269
|
+
"
|
|
270
|
+
`)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test('handles single row table', () => {
|
|
274
|
+
const input = `| A | B | C | D |
|
|
275
|
+
| --- | --- | --- | --- |
|
|
276
|
+
| 1 | 2 | 3 | 4 |`
|
|
277
|
+
const result = formatMarkdownTables(input)
|
|
278
|
+
expect(result).toMatchInlineSnapshot(`
|
|
279
|
+
"\`\`\`
|
|
280
|
+
A B C D
|
|
281
|
+
- - - -
|
|
282
|
+
1 2 3 4
|
|
283
|
+
\`\`\`
|
|
284
|
+
"
|
|
285
|
+
`)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('handles nested formatting', () => {
|
|
289
|
+
const input = `| Description |
|
|
290
|
+
| --- |
|
|
291
|
+
| **Bold with *nested italic* inside** |
|
|
292
|
+
| *Italic with **nested bold** inside* |`
|
|
293
|
+
const result = formatMarkdownTables(input)
|
|
294
|
+
expect(result).toMatchInlineSnapshot(`
|
|
295
|
+
"\`\`\`
|
|
296
|
+
Description
|
|
297
|
+
------------------------------
|
|
298
|
+
Bold with nested italic inside
|
|
299
|
+
Italic with nested bold inside
|
|
300
|
+
\`\`\`
|
|
301
|
+
"
|
|
302
|
+
`)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('handles image references', () => {
|
|
306
|
+
const input = `| Icon | Name |
|
|
307
|
+
| --- | --- |
|
|
308
|
+
|  | Item 1 |
|
|
309
|
+
|  | Item 2 |`
|
|
310
|
+
const result = formatMarkdownTables(input)
|
|
311
|
+
expect(result).toMatchInlineSnapshot(`
|
|
312
|
+
"\`\`\`
|
|
313
|
+
Icon Name
|
|
314
|
+
---------------------------- ------
|
|
315
|
+
https://example.com/icon.png Item 1
|
|
316
|
+
https://cdn.test.com/img.jpg Item 2
|
|
317
|
+
\`\`\`
|
|
318
|
+
"
|
|
319
|
+
`)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('preserves code blocks alongside tables', () => {
|
|
323
|
+
const input = `Some code:
|
|
324
|
+
|
|
325
|
+
\`\`\`js
|
|
326
|
+
const x = 1
|
|
327
|
+
\`\`\`
|
|
328
|
+
|
|
329
|
+
A table:
|
|
330
|
+
|
|
331
|
+
| Key | Value |
|
|
332
|
+
| --- | --- |
|
|
333
|
+
| a | 1 |
|
|
334
|
+
|
|
335
|
+
More code:
|
|
336
|
+
|
|
337
|
+
\`\`\`python
|
|
338
|
+
print("hello")
|
|
339
|
+
\`\`\``
|
|
340
|
+
const result = formatMarkdownTables(input)
|
|
341
|
+
expect(result).toMatchInlineSnapshot(`
|
|
342
|
+
"Some code:
|
|
343
|
+
|
|
344
|
+
\`\`\`js
|
|
345
|
+
const x = 1
|
|
346
|
+
\`\`\`
|
|
347
|
+
|
|
348
|
+
A table:
|
|
349
|
+
|
|
350
|
+
\`\`\`
|
|
351
|
+
Key Value
|
|
352
|
+
--- -----
|
|
353
|
+
a 1
|
|
354
|
+
\`\`\`
|
|
355
|
+
More code:
|
|
356
|
+
|
|
357
|
+
\`\`\`python
|
|
358
|
+
print("hello")
|
|
359
|
+
\`\`\`"
|
|
360
|
+
`)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
test('handles content without tables', () => {
|
|
364
|
+
const input = `Just some regular markdown.
|
|
365
|
+
|
|
366
|
+
- List item 1
|
|
367
|
+
- List item 2
|
|
368
|
+
|
|
369
|
+
**Bold text** and *italic*.`
|
|
370
|
+
const result = formatMarkdownTables(input)
|
|
371
|
+
expect(result).toMatchInlineSnapshot(`
|
|
372
|
+
"Just some regular markdown.
|
|
373
|
+
|
|
374
|
+
- List item 1
|
|
375
|
+
- List item 2
|
|
376
|
+
|
|
377
|
+
**Bold text** and *italic*."
|
|
378
|
+
`)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
test('handles complex real-world table', () => {
|
|
382
|
+
const input = `## API Endpoints
|
|
383
|
+
|
|
384
|
+
| Method | Endpoint | Description | Auth |
|
|
385
|
+
| --- | --- | --- | --- |
|
|
386
|
+
| GET | \`/api/users\` | List all users | [Bearer token](https://docs.example.com/auth) |
|
|
387
|
+
| POST | \`/api/users\` | Create **new** user | Required |
|
|
388
|
+
| DELETE | \`/api/users/:id\` | ~~Remove~~ *Deactivate* user | Admin only |`
|
|
389
|
+
const result = formatMarkdownTables(input)
|
|
390
|
+
expect(result).toMatchInlineSnapshot(`
|
|
391
|
+
"## API Endpoints
|
|
392
|
+
|
|
393
|
+
\`\`\`
|
|
394
|
+
Method Endpoint Description Auth
|
|
395
|
+
------ -------------- ---------------------- -----------------------------
|
|
396
|
+
GET /api/users List all users https://docs.example.com/auth
|
|
397
|
+
POST /api/users Create new user Required
|
|
398
|
+
DELETE /api/users/:id Remove Deactivate user Admin only
|
|
399
|
+
\`\`\`
|
|
400
|
+
"
|
|
401
|
+
`)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
test('handles unicode content', () => {
|
|
405
|
+
const input = `| Emoji | Name | Country |
|
|
406
|
+
| --- | --- | --- |
|
|
407
|
+
| 🍎 | Apple | 日本 |
|
|
408
|
+
| 🍊 | Orange | España |
|
|
409
|
+
| 🍌 | Banana | Ελλάδα |`
|
|
410
|
+
const result = formatMarkdownTables(input)
|
|
411
|
+
expect(result).toMatchInlineSnapshot(`
|
|
412
|
+
"\`\`\`
|
|
413
|
+
Emoji Name Country
|
|
414
|
+
----- ------ -------
|
|
415
|
+
🍎 Apple 日本
|
|
416
|
+
🍊 Orange España
|
|
417
|
+
🍌 Banana Ελλάδα
|
|
418
|
+
\`\`\`
|
|
419
|
+
"
|
|
420
|
+
`)
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
test('handles numbers and special characters', () => {
|
|
424
|
+
const input = `| Price | Discount | Final |
|
|
425
|
+
| --- | --- | --- |
|
|
426
|
+
| $100.00 | -15% | $85.00 |
|
|
427
|
+
| €50,00 | -10% | €45,00 |
|
|
428
|
+
| £75.99 | N/A | £75.99 |`
|
|
429
|
+
const result = formatMarkdownTables(input)
|
|
430
|
+
expect(result).toMatchInlineSnapshot(`
|
|
431
|
+
"\`\`\`
|
|
432
|
+
Price Discount Final
|
|
433
|
+
------- -------- ------
|
|
434
|
+
$100.00 -15% $85.00
|
|
435
|
+
€50,00 -10% €45,00
|
|
436
|
+
£75.99 N/A £75.99
|
|
437
|
+
\`\`\`
|
|
438
|
+
"
|
|
439
|
+
`)
|
|
440
|
+
})
|