Skip to content

Commit 2364293

Browse files
Merge pull request #71 from ai-action/fix/messages
2 parents 9e4c3cb + 69cffe7 commit 2364293

8 files changed

Lines changed: 795 additions & 55 deletions

File tree

src/components/CodeBlock/CodeBlock.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,30 @@ interface CodeBlockProps {
1111

1212
const highlightCache = new Map<string, string>();
1313

14-
export const CODE_BLOCK_REGEX = /^(`{3,})(\w+)?[ \t]*\n([\s\S]*?)^\1[ \t]*$/gm;
14+
const CODE_BLOCK_REGEX =
15+
/^(?<indent>[ \t]*)(`{3,})(\w+)?[ \t]*\n([\s\S]*?)^\k<indent>\2[ \t]*$/gm;
16+
17+
export function normalizeCodeBlockContent(
18+
content: string,
19+
indent = '',
20+
): string {
21+
if (!indent) {
22+
return content.trim();
23+
}
24+
25+
const indentPattern = new RegExp(`^${indent}`, 'gm');
26+
return content.replace(indentPattern, '').trim();
27+
}
1528

1629
export async function prewarmCodeBlocks(content: string): Promise<void> {
1730
const promises: Promise<void>[] = [];
1831
let match;
1932
CODE_BLOCK_REGEX.lastIndex = 0;
2033

2134
while ((match = CODE_BLOCK_REGEX.exec(content)) !== null) {
22-
const language = match[2];
23-
const code = match[3].trim();
35+
const indent = match[1];
36+
const language = match[3];
37+
const code = normalizeCodeBlockContent(match[4], indent);
2438
// v8 ignore next 2
2539
if (code) {
2640
promises.push(prewarmHighlight(code, language));

src/components/Markdown/Markdown.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,38 @@
1+
import { useStdout } from 'ink';
12
import { render } from 'ink-testing-library';
23

34
import { UI } from '../../constants';
45
import { Markdown } from './Markdown';
56

7+
const { mockColumns } = vi.hoisted(() => ({
8+
mockColumns: {
9+
value: 100,
10+
},
11+
}));
12+
13+
vi.mock('ink', async () => ({
14+
...(await vi.importActual('ink')),
15+
useStdout: vi.fn(() => ({
16+
stdout: {
17+
columns: mockColumns.value,
18+
},
19+
})),
20+
}));
21+
22+
function setTerminalWidth(columns: number) {
23+
mockColumns.value = columns;
24+
}
25+
26+
function stripAnsi(value: string | undefined) {
27+
return value?.replaceAll(new RegExp(String.raw`\u001B\[[0-9;]*m`, 'g'), '');
28+
}
29+
630
describe('Markdown', () => {
31+
beforeEach(() => {
32+
setTerminalWidth(100);
33+
vi.mocked(useStdout).mockClear();
34+
});
35+
736
it('renders markdown content', () => {
837
const { lastFrame } = render(<Markdown content="# Hello" />);
938
expect(lastFrame()).toContain('Hello');
@@ -79,4 +108,18 @@ describe('Markdown', () => {
79108
const { lastFrame } = render(<Markdown content="$dx \\, dt$" />);
80109
expect(lastFrame()).not.toContain('\\,');
81110
});
111+
112+
it('reflows wrapped markdown lists before Ink wraps ANSI output', () => {
113+
setTerminalWidth(40);
114+
115+
const content =
116+
'4. **Restructure the "Usage" section** to clearly separate **Interactive TUI** from **CLI Commands**.';
117+
118+
const { lastFrame } = render(<Markdown content={content} />);
119+
const frame = stripAnsi(lastFrame()) ?? '';
120+
121+
expect(frame).toContain('CLI');
122+
expect(frame).toContain('Commands');
123+
expect(frame).not.toContain('**');
124+
});
82125
});

src/components/Markdown/Markdown.tsx

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Text, useStdout } from 'ink';
2-
import { marked } from 'marked';
2+
import { Marked, type Token } from 'marked';
33
import { markedTerminal } from 'marked-terminal';
44
import { memo, useMemo } from 'react';
55

@@ -14,24 +14,41 @@ interface MarkdownProps {
1414

1515
const HR_PLACEHOLDER = '__CODE_OLLAMA_HR_PLACEHOLDER__';
1616

17-
marked.use(
18-
markedTerminal({
19-
theme: 'gitHub',
20-
}),
21-
);
22-
23-
marked.use({
24-
extensions: [inlineMathExtension],
25-
renderer: {
26-
hr: () => `${HR_PLACEHOLDER}\n`,
27-
},
28-
});
29-
3017
function renderMarkdown(content: string, hrWidth: number): string {
3118
const hr = UI.MARKDOWN_HR_CHARACTER.repeat(Math.max(1, hrWidth));
19+
const markdown = new Marked();
20+
const rendererExtension = {
21+
extensions: [inlineMathExtension],
22+
useNewRenderer: true,
23+
renderer: {
24+
hr: () => `${HR_PLACEHOLDER}\n`,
25+
text(token: Token) {
26+
const textToken = token as Token & {
27+
text?: string;
28+
tokens?: Token[];
29+
};
30+
31+
if (typeof token === 'object' && Array.isArray(textToken.tokens)) {
32+
return this.parser.parseInline(textToken.tokens);
33+
}
34+
35+
return String(textToken.text);
36+
},
37+
},
38+
} as Parameters<Marked['use']>[0];
39+
40+
markdown.use(
41+
markedTerminal({
42+
theme: 'gitHub',
43+
reflowText: true,
44+
width: Math.max(1, hrWidth),
45+
}),
46+
);
47+
48+
markdown.use(rendererExtension);
3249

3350
try {
34-
const result = marked.parse(content);
51+
const result = markdown.parse(content);
3552
// v8 ignore start
3653
const text = typeof result === 'string' ? result.trim() : content;
3754
return text.replaceAll(HR_PLACEHOLDER, hr);

0 commit comments

Comments
 (0)