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.
@@ -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
- // parenthesized digits ⑴-⒇ for 1-20, fallback to regular number for 21+
190
- const parenthesizedDigits = '⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇';
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 ? parenthesizedDigits[todoNumber - 1] : `(${todoNumber})`;
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(`" **second task**"`);
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(`" **task 12**"`);
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(`" **fix the bug**"`);
71
+ expect(formatTodoList(part)).toMatchInlineSnapshot(`" **fix the bug**"`);
72
72
  });
73
73
  });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.32",
5
+ "version": "0.4.33",
6
6
  "scripts": {
7
7
  "dev": "tsx --env-file .env src/cli.ts",
8
8
  "prepublishOnly": "pnpm tsc",
@@ -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(`" **second task**"`)
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(`" **task 12**"`)
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(`" **fix the bug**"`)
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
- // parenthesized digits ⑴-⒇ for 1-20, fallback to regular number for 21+
249
- const parenthesizedDigits = '⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇'
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 ? parenthesizedDigits[todoNumber - 1] : `(${todoNumber})`
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
  }