kimaki 0.4.32 → 0.4.33
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/discord-utils.js +2 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/message-formatting.js +3 -3
- package/dist/message-formatting.test.js +3 -3
- package/package.json +1 -1
- package/src/discord-utils.ts +2 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/message-formatting.test.ts +3 -3
- package/src/message-formatting.ts +3 -3
package/dist/discord-utils.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ChannelType, } from 'discord.js';
|
|
|
5
5
|
import { Lexer } from 'marked';
|
|
6
6
|
import { extractTagsArrays } from './xml.js';
|
|
7
7
|
import { formatMarkdownTables } from './format-tables.js';
|
|
8
|
+
import { limitHeadingDepth } from './limit-heading-depth.js';
|
|
8
9
|
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js';
|
|
9
10
|
import { createLogger } from './logger.js';
|
|
10
11
|
const discordLogger = createLogger('DISCORD');
|
|
@@ -159,6 +160,7 @@ export async function sendThreadMessage(thread, content, options) {
|
|
|
159
160
|
const MAX_LENGTH = 2000;
|
|
160
161
|
content = formatMarkdownTables(content);
|
|
161
162
|
content = unnestCodeBlocksFromLists(content);
|
|
163
|
+
content = limitHeadingDepth(content);
|
|
162
164
|
content = escapeBackticksInCodeBlocks(content);
|
|
163
165
|
// If custom flags provided, send as single message (no chunking)
|
|
164
166
|
if (options?.flags !== undefined) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Limit heading depth for Discord.
|
|
2
|
+
// Discord only supports headings up to ### (h3), so this converts
|
|
3
|
+
// ####, #####, etc. to ### to maintain consistent rendering.
|
|
4
|
+
import { Lexer } from 'marked';
|
|
5
|
+
export function limitHeadingDepth(markdown, maxDepth = 3) {
|
|
6
|
+
const lexer = new Lexer();
|
|
7
|
+
const tokens = lexer.lex(markdown);
|
|
8
|
+
let result = '';
|
|
9
|
+
for (const token of tokens) {
|
|
10
|
+
if (token.type === 'heading') {
|
|
11
|
+
const heading = token;
|
|
12
|
+
if (heading.depth > maxDepth) {
|
|
13
|
+
const hashes = '#'.repeat(maxDepth);
|
|
14
|
+
result += hashes + ' ' + heading.text + '\n';
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
result += token.raw;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
result += token.raw;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { limitHeadingDepth } from './limit-heading-depth.js';
|
|
3
|
+
test('converts h4 to h3', () => {
|
|
4
|
+
const input = '#### Fourth level heading';
|
|
5
|
+
const result = limitHeadingDepth(input);
|
|
6
|
+
expect(result).toMatchInlineSnapshot(`
|
|
7
|
+
"### Fourth level heading
|
|
8
|
+
"
|
|
9
|
+
`);
|
|
10
|
+
});
|
|
11
|
+
test('converts h5 to h3', () => {
|
|
12
|
+
const input = '##### Fifth level heading';
|
|
13
|
+
const result = limitHeadingDepth(input);
|
|
14
|
+
expect(result).toMatchInlineSnapshot(`
|
|
15
|
+
"### Fifth level heading
|
|
16
|
+
"
|
|
17
|
+
`);
|
|
18
|
+
});
|
|
19
|
+
test('converts h6 to h3', () => {
|
|
20
|
+
const input = '###### Sixth level heading';
|
|
21
|
+
const result = limitHeadingDepth(input);
|
|
22
|
+
expect(result).toMatchInlineSnapshot(`
|
|
23
|
+
"### Sixth level heading
|
|
24
|
+
"
|
|
25
|
+
`);
|
|
26
|
+
});
|
|
27
|
+
test('preserves h3 unchanged', () => {
|
|
28
|
+
const input = '### Third level heading';
|
|
29
|
+
const result = limitHeadingDepth(input);
|
|
30
|
+
expect(result).toMatchInlineSnapshot(`"### Third level heading"`);
|
|
31
|
+
});
|
|
32
|
+
test('preserves h2 unchanged', () => {
|
|
33
|
+
const input = '## Second level heading';
|
|
34
|
+
const result = limitHeadingDepth(input);
|
|
35
|
+
expect(result).toMatchInlineSnapshot(`"## Second level heading"`);
|
|
36
|
+
});
|
|
37
|
+
test('preserves h1 unchanged', () => {
|
|
38
|
+
const input = '# First level heading';
|
|
39
|
+
const result = limitHeadingDepth(input);
|
|
40
|
+
expect(result).toMatchInlineSnapshot(`"# First level heading"`);
|
|
41
|
+
});
|
|
42
|
+
test('handles multiple headings in document', () => {
|
|
43
|
+
const input = `# Title
|
|
44
|
+
|
|
45
|
+
Some text
|
|
46
|
+
|
|
47
|
+
## Section
|
|
48
|
+
|
|
49
|
+
### Subsection
|
|
50
|
+
|
|
51
|
+
#### Too deep
|
|
52
|
+
|
|
53
|
+
##### Even deeper
|
|
54
|
+
|
|
55
|
+
Regular paragraph
|
|
56
|
+
|
|
57
|
+
### Back to normal
|
|
58
|
+
`;
|
|
59
|
+
const result = limitHeadingDepth(input);
|
|
60
|
+
expect(result).toMatchInlineSnapshot(`
|
|
61
|
+
"# Title
|
|
62
|
+
|
|
63
|
+
Some text
|
|
64
|
+
|
|
65
|
+
## Section
|
|
66
|
+
|
|
67
|
+
### Subsection
|
|
68
|
+
|
|
69
|
+
### Too deep
|
|
70
|
+
### Even deeper
|
|
71
|
+
Regular paragraph
|
|
72
|
+
|
|
73
|
+
### Back to normal
|
|
74
|
+
"
|
|
75
|
+
`);
|
|
76
|
+
});
|
|
77
|
+
test('preserves heading with inline formatting', () => {
|
|
78
|
+
const input = '#### Heading with **bold** and `code`';
|
|
79
|
+
const result = limitHeadingDepth(input);
|
|
80
|
+
expect(result).toMatchInlineSnapshot(`
|
|
81
|
+
"### Heading with **bold** and \`code\`
|
|
82
|
+
"
|
|
83
|
+
`);
|
|
84
|
+
});
|
|
85
|
+
test('handles empty markdown', () => {
|
|
86
|
+
const result = limitHeadingDepth('');
|
|
87
|
+
expect(result).toMatchInlineSnapshot(`""`);
|
|
88
|
+
});
|
|
89
|
+
test('handles markdown with no headings', () => {
|
|
90
|
+
const input = 'Just some text\n\nAnd more text';
|
|
91
|
+
const result = limitHeadingDepth(input);
|
|
92
|
+
expect(result).toMatchInlineSnapshot(`
|
|
93
|
+
"Just some text
|
|
94
|
+
|
|
95
|
+
And more text"
|
|
96
|
+
`);
|
|
97
|
+
});
|
|
98
|
+
test('allows custom maxDepth', () => {
|
|
99
|
+
const input = '### Third level';
|
|
100
|
+
const result = limitHeadingDepth(input, 2);
|
|
101
|
+
expect(result).toMatchInlineSnapshot(`
|
|
102
|
+
"## Third level
|
|
103
|
+
"
|
|
104
|
+
`);
|
|
105
|
+
});
|
|
@@ -186,10 +186,10 @@ export function formatTodoList(part) {
|
|
|
186
186
|
const activeTodo = todos[activeIndex];
|
|
187
187
|
if (activeIndex === -1 || !activeTodo)
|
|
188
188
|
return '';
|
|
189
|
-
//
|
|
190
|
-
const
|
|
189
|
+
// digit-with-period ⒈-⒛ for 1-20, fallback to regular number for 21+
|
|
190
|
+
const digitWithPeriod = '⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛';
|
|
191
191
|
const todoNumber = activeIndex + 1;
|
|
192
|
-
const num = todoNumber <= 20 ?
|
|
192
|
+
const num = todoNumber <= 20 ? digitWithPeriod[todoNumber - 1] : `${todoNumber}.`;
|
|
193
193
|
const content = activeTodo.content.charAt(0).toLowerCase() + activeTodo.content.slice(1);
|
|
194
194
|
return `${num} **${escapeInlineMarkdown(content)}**`;
|
|
195
195
|
}
|
|
@@ -24,7 +24,7 @@ describe('formatTodoList', () => {
|
|
|
24
24
|
time: { start: 0, end: 0 },
|
|
25
25
|
},
|
|
26
26
|
};
|
|
27
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
27
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒉ **second task**"`);
|
|
28
28
|
});
|
|
29
29
|
test('formats double digit todo numbers', () => {
|
|
30
30
|
const todos = Array.from({ length: 12 }, (_, i) => ({
|
|
@@ -47,7 +47,7 @@ describe('formatTodoList', () => {
|
|
|
47
47
|
time: { start: 0, end: 0 },
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
50
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒓ **task 12**"`);
|
|
51
51
|
});
|
|
52
52
|
test('lowercases first letter of content', () => {
|
|
53
53
|
const part = {
|
|
@@ -68,6 +68,6 @@ describe('formatTodoList', () => {
|
|
|
68
68
|
time: { start: 0, end: 0 },
|
|
69
69
|
},
|
|
70
70
|
};
|
|
71
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
71
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒈ **fix the bug**"`);
|
|
72
72
|
});
|
|
73
73
|
});
|
package/package.json
CHANGED
package/src/discord-utils.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { Lexer } from 'marked'
|
|
12
12
|
import { extractTagsArrays } from './xml.js'
|
|
13
13
|
import { formatMarkdownTables } from './format-tables.js'
|
|
14
|
+
import { limitHeadingDepth } from './limit-heading-depth.js'
|
|
14
15
|
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js'
|
|
15
16
|
import { createLogger } from './logger.js'
|
|
16
17
|
|
|
@@ -201,6 +202,7 @@ export async function sendThreadMessage(
|
|
|
201
202
|
|
|
202
203
|
content = formatMarkdownTables(content)
|
|
203
204
|
content = unnestCodeBlocksFromLists(content)
|
|
205
|
+
content = limitHeadingDepth(content)
|
|
204
206
|
content = escapeBackticksInCodeBlocks(content)
|
|
205
207
|
|
|
206
208
|
// If custom flags provided, send as single message (no chunking)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
import { limitHeadingDepth } from './limit-heading-depth.js'
|
|
3
|
+
|
|
4
|
+
test('converts h4 to h3', () => {
|
|
5
|
+
const input = '#### Fourth level heading'
|
|
6
|
+
const result = limitHeadingDepth(input)
|
|
7
|
+
expect(result).toMatchInlineSnapshot(`
|
|
8
|
+
"### Fourth level heading
|
|
9
|
+
"
|
|
10
|
+
`)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('converts h5 to h3', () => {
|
|
14
|
+
const input = '##### Fifth level heading'
|
|
15
|
+
const result = limitHeadingDepth(input)
|
|
16
|
+
expect(result).toMatchInlineSnapshot(`
|
|
17
|
+
"### Fifth level heading
|
|
18
|
+
"
|
|
19
|
+
`)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('converts h6 to h3', () => {
|
|
23
|
+
const input = '###### Sixth level heading'
|
|
24
|
+
const result = limitHeadingDepth(input)
|
|
25
|
+
expect(result).toMatchInlineSnapshot(`
|
|
26
|
+
"### Sixth level heading
|
|
27
|
+
"
|
|
28
|
+
`)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('preserves h3 unchanged', () => {
|
|
32
|
+
const input = '### Third level heading'
|
|
33
|
+
const result = limitHeadingDepth(input)
|
|
34
|
+
expect(result).toMatchInlineSnapshot(`"### Third level heading"`)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('preserves h2 unchanged', () => {
|
|
38
|
+
const input = '## Second level heading'
|
|
39
|
+
const result = limitHeadingDepth(input)
|
|
40
|
+
expect(result).toMatchInlineSnapshot(`"## Second level heading"`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('preserves h1 unchanged', () => {
|
|
44
|
+
const input = '# First level heading'
|
|
45
|
+
const result = limitHeadingDepth(input)
|
|
46
|
+
expect(result).toMatchInlineSnapshot(`"# First level heading"`)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('handles multiple headings in document', () => {
|
|
50
|
+
const input = `# Title
|
|
51
|
+
|
|
52
|
+
Some text
|
|
53
|
+
|
|
54
|
+
## Section
|
|
55
|
+
|
|
56
|
+
### Subsection
|
|
57
|
+
|
|
58
|
+
#### Too deep
|
|
59
|
+
|
|
60
|
+
##### Even deeper
|
|
61
|
+
|
|
62
|
+
Regular paragraph
|
|
63
|
+
|
|
64
|
+
### Back to normal
|
|
65
|
+
`
|
|
66
|
+
const result = limitHeadingDepth(input)
|
|
67
|
+
expect(result).toMatchInlineSnapshot(`
|
|
68
|
+
"# Title
|
|
69
|
+
|
|
70
|
+
Some text
|
|
71
|
+
|
|
72
|
+
## Section
|
|
73
|
+
|
|
74
|
+
### Subsection
|
|
75
|
+
|
|
76
|
+
### Too deep
|
|
77
|
+
### Even deeper
|
|
78
|
+
Regular paragraph
|
|
79
|
+
|
|
80
|
+
### Back to normal
|
|
81
|
+
"
|
|
82
|
+
`)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('preserves heading with inline formatting', () => {
|
|
86
|
+
const input = '#### Heading with **bold** and `code`'
|
|
87
|
+
const result = limitHeadingDepth(input)
|
|
88
|
+
expect(result).toMatchInlineSnapshot(`
|
|
89
|
+
"### Heading with **bold** and \`code\`
|
|
90
|
+
"
|
|
91
|
+
`)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('handles empty markdown', () => {
|
|
95
|
+
const result = limitHeadingDepth('')
|
|
96
|
+
expect(result).toMatchInlineSnapshot(`""`)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('handles markdown with no headings', () => {
|
|
100
|
+
const input = 'Just some text\n\nAnd more text'
|
|
101
|
+
const result = limitHeadingDepth(input)
|
|
102
|
+
expect(result).toMatchInlineSnapshot(`
|
|
103
|
+
"Just some text
|
|
104
|
+
|
|
105
|
+
And more text"
|
|
106
|
+
`)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('allows custom maxDepth', () => {
|
|
110
|
+
const input = '### Third level'
|
|
111
|
+
const result = limitHeadingDepth(input, 2)
|
|
112
|
+
expect(result).toMatchInlineSnapshot(`
|
|
113
|
+
"## Third level
|
|
114
|
+
"
|
|
115
|
+
`)
|
|
116
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Limit heading depth for Discord.
|
|
2
|
+
// Discord only supports headings up to ### (h3), so this converts
|
|
3
|
+
// ####, #####, etc. to ### to maintain consistent rendering.
|
|
4
|
+
|
|
5
|
+
import { Lexer, type Tokens } from 'marked'
|
|
6
|
+
|
|
7
|
+
export function limitHeadingDepth(markdown: string, maxDepth = 3): string {
|
|
8
|
+
const lexer = new Lexer()
|
|
9
|
+
const tokens = lexer.lex(markdown)
|
|
10
|
+
|
|
11
|
+
let result = ''
|
|
12
|
+
for (const token of tokens) {
|
|
13
|
+
if (token.type === 'heading') {
|
|
14
|
+
const heading = token as Tokens.Heading
|
|
15
|
+
if (heading.depth > maxDepth) {
|
|
16
|
+
const hashes = '#'.repeat(maxDepth)
|
|
17
|
+
result += hashes + ' ' + heading.text + '\n'
|
|
18
|
+
} else {
|
|
19
|
+
result += token.raw
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
result += token.raw
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
@@ -27,7 +27,7 @@ describe('formatTodoList', () => {
|
|
|
27
27
|
},
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
30
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒉ **second task**"`)
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
test('formats double digit todo numbers', () => {
|
|
@@ -53,7 +53,7 @@ describe('formatTodoList', () => {
|
|
|
53
53
|
},
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
56
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒓ **task 12**"`)
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
test('lowercases first letter of content', () => {
|
|
@@ -76,6 +76,6 @@ describe('formatTodoList', () => {
|
|
|
76
76
|
},
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
expect(formatTodoList(part)).toMatchInlineSnapshot(`"
|
|
79
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒈ **fix the bug**"`)
|
|
80
80
|
})
|
|
81
81
|
})
|
|
@@ -245,10 +245,10 @@ export function formatTodoList(part: Part): string {
|
|
|
245
245
|
})
|
|
246
246
|
const activeTodo = todos[activeIndex]
|
|
247
247
|
if (activeIndex === -1 || !activeTodo) return ''
|
|
248
|
-
//
|
|
249
|
-
const
|
|
248
|
+
// digit-with-period ⒈-⒛ for 1-20, fallback to regular number for 21+
|
|
249
|
+
const digitWithPeriod = '⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛'
|
|
250
250
|
const todoNumber = activeIndex + 1
|
|
251
|
-
const num = todoNumber <= 20 ?
|
|
251
|
+
const num = todoNumber <= 20 ? digitWithPeriod[todoNumber - 1] : `${todoNumber}.`
|
|
252
252
|
const content = activeTodo.content.charAt(0).toLowerCase() + activeTodo.content.slice(1)
|
|
253
253
|
return `${num} **${escapeInlineMarkdown(content)}**`
|
|
254
254
|
}
|