Fix markdown rendering on Windows (#10185)

This commit is contained in:
Jacob Richman
2025-09-29 13:38:27 -07:00
committed by GitHub
parent 5478b58166
commit 94f43c79d0
3 changed files with 238 additions and 145 deletions
+131 -125
View File
@@ -7,7 +7,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MarkdownDisplay } from './MarkdownDisplay.js'; import { MarkdownDisplay } from './MarkdownDisplay.js';
import { LoadedSettings } from '../../config/settings.js'; import { LoadedSettings } from '../../config/settings.js';
import { EOL } from 'node:os';
import { renderWithProviders } from '../../test-utils/render.js'; import { renderWithProviders } from '../../test-utils/render.js';
describe('<MarkdownDisplay />', () => { describe('<MarkdownDisplay />', () => {
@@ -36,132 +35,138 @@ describe('<MarkdownDisplay />', () => {
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('renders headers with correct levels', () => { const lineEndings = [
const text = ` { name: 'Windows', eol: '\r\n' },
{ name: 'Unix', eol: '\n' },
];
describe.each(lineEndings)('with $name line endings', ({ eol }) => {
it('renders headers with correct levels', () => {
const text = `
# Header 1 # Header 1
## Header 2 ## Header 2
### Header 3 ### Header 3
#### Header 4 #### Header 4
`.replace(/\n/g, EOL); `.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('renders a fenced code block with a language', () => { it('renders a fenced code block with a language', () => {
const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```'.replace( const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```'.replace(
/\n/g, /\n/g,
EOL, eol,
); );
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('renders a fenced code block without a language', () => { it('renders a fenced code block without a language', () => {
const text = '```\nplain text\n```'.replace(/\n/g, EOL); const text = '```\nplain text\n```'.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('handles unclosed (pending) code blocks', () => { it('handles unclosed (pending) code blocks', () => {
const text = '```typescript\nlet y = 2;'.replace(/\n/g, EOL); const text = '```typescript\nlet y = 2;'.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} isPending={true} />, <MarkdownDisplay {...baseProps} text={text} isPending={true} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('renders unordered lists with different markers', () => { it('renders unordered lists with different markers', () => {
const text = ` const text = `
- item A - item A
* item B * item B
+ item C + item C
`.replace(/\n/g, EOL); `.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('renders nested unordered lists', () => { it('renders nested unordered lists', () => {
const text = ` const text = `
* Level 1 * Level 1
* Level 2 * Level 2
* Level 3 * Level 3
`.replace(/\n/g, EOL); `.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('renders ordered lists', () => { it('renders ordered lists', () => {
const text = ` const text = `
1. First item 1. First item
2. Second item 2. Second item
`.replace(/\n/g, EOL); `.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('renders horizontal rules', () => { it('renders horizontal rules', () => {
const text = ` const text = `
Hello Hello
--- ---
World World
*** ***
Test Test
`.replace(/\n/g, EOL); `.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('renders tables correctly', () => { it('renders tables correctly', () => {
const text = ` const text = `
| Header 1 | Header 2 | | Header 1 | Header 2 |
|----------|:--------:| |----------|:--------:|
| Cell 1 | Cell 2 | | Cell 1 | Cell 2 |
| Cell 3 | Cell 4 | | Cell 3 | Cell 4 |
`.replace(/\n/g, EOL); `.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('handles a table at the end of the input', () => { it('handles a table at the end of the input', () => {
const text = ` const text = `
Some text before. Some text before.
| A | B | | A | B |
|---| |---|
| 1 | 2 |`.replace(/\n/g, EOL); | 1 | 2 |`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('inserts a single space between paragraphs', () => { it('inserts a single space between paragraphs', () => {
const text = `Paragraph 1. const text = `Paragraph 1.
Paragraph 2.`.replace(/\n/g, EOL); Paragraph 2.`.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('correctly parses a mix of markdown elements', () => { it('correctly parses a mix of markdown elements', () => {
const text = ` const text = `
# Main Title # Main Title
Here is a paragraph. Here is a paragraph.
@@ -174,42 +179,43 @@ some code
\`\`\` \`\`\`
Another paragraph. Another paragraph.
`.replace(/\n/g, EOL); `.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('hides line numbers in code blocks when showLineNumbers is false', () => { it('hides line numbers in code blocks when showLineNumbers is false', () => {
const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, EOL); const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, eol);
const settings = new LoadedSettings( const settings = new LoadedSettings(
{ path: '', settings: {}, originalSettings: {} }, { path: '', settings: {}, originalSettings: {} },
{ path: '', settings: {}, originalSettings: {} }, { path: '', settings: {}, originalSettings: {} },
{ {
path: '', path: '',
settings: { ui: { showLineNumbers: false } }, settings: { ui: { showLineNumbers: false } },
originalSettings: { ui: { showLineNumbers: false } }, originalSettings: { ui: { showLineNumbers: false } },
}, },
{ path: '', settings: {}, originalSettings: {} }, { path: '', settings: {}, originalSettings: {} },
true, true,
new Set(), new Set(),
); );
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
{ settings }, { settings },
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
expect(lastFrame()).not.toContain(' 1 '); expect(lastFrame()).not.toContain(' 1 ');
}); });
it('shows line numbers in code blocks by default', () => { it('shows line numbers in code blocks by default', () => {
const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, EOL); const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, eol);
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<MarkdownDisplay {...baseProps} text={text} />, <MarkdownDisplay {...baseProps} text={text} />,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
expect(lastFrame()).toContain(' 1 '); expect(lastFrame()).toContain(' 1 ');
});
}); });
}); });
@@ -6,7 +6,6 @@
import React from 'react'; import React from 'react';
import { Text, Box } from 'ink'; import { Text, Box } from 'ink';
import { EOL } from 'node:os';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { colorizeCode } from './CodeColorizer.js'; import { colorizeCode } from './CodeColorizer.js';
import { TableRenderer } from './TableRenderer.js'; import { TableRenderer } from './TableRenderer.js';
@@ -35,7 +34,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
}) => { }) => {
if (!text) return <></>; if (!text) return <></>;
const lines = text.split(EOL); const lines = text.split(/\r?\n/);
const headerRegex = /^ *(#{1,4}) +(.*)/; const headerRegex = /^ *(#{1,4}) +(.*)/;
const codeFenceRegex = /^ *(`{3,}|~{3,}) *(\w*?) *$/; const codeFenceRegex = /^ *(`{3,}|~{3,}) *(\w*?) *$/;
const ulItemRegex = /^([ \t]*)([-*+]) +(.*)/; const ulItemRegex = /^([ \t]*)([-*+]) +(.*)/;
@@ -1,6 +1,10 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<MarkdownDisplay /> > correctly parses a mix of markdown elements 1`] = ` exports[`<MarkdownDisplay /> > renders a simple paragraph 1`] = `"Hello, world."`;
exports[`<MarkdownDisplay /> > renders nothing for empty text 1`] = `""`;
exports[`<MarkdownDisplay /> > with 'Unix' line endings > correctly parses a mix of markdown elements 1`] = `
"Main Title "Main Title
Here is a paragraph. Here is a paragraph.
@@ -14,33 +18,31 @@ Another paragraph.
" "
`; `;
exports[`<MarkdownDisplay /> > handles a table at the end of the input 1`] = ` exports[`<MarkdownDisplay /> > with 'Unix' line endings > handles a table at the end of the input 1`] = `
"Some text before. "Some text before.
| A | B | | A | B |
|---| |---|
| 1 | 2 |" | 1 | 2 |"
`; `;
exports[`<MarkdownDisplay /> > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`; exports[`<MarkdownDisplay /> > with 'Unix' line endings > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`;
exports[`<MarkdownDisplay /> > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`; exports[`<MarkdownDisplay /> > with 'Unix' line endings > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`;
exports[`<MarkdownDisplay /> > inserts a single space between paragraphs 1`] = ` exports[`<MarkdownDisplay /> > with 'Unix' line endings > inserts a single space between paragraphs 1`] = `
"Paragraph 1. "Paragraph 1.
Paragraph 2." Paragraph 2."
`; `;
exports[`<MarkdownDisplay /> > renders a fenced code block with a language 1`] = ` exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders a fenced code block with a language 1`] = `
" 1 const x = 1; " 1 const x = 1;
2 console.log(x);" 2 console.log(x);"
`; `;
exports[`<MarkdownDisplay /> > renders a fenced code block without a language 1`] = `" 1 plain text"`; exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders a fenced code block without a language 1`] = `" 1 plain text"`;
exports[`<MarkdownDisplay /> > renders a simple paragraph 1`] = `"Hello, world."`; exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders headers with correct levels 1`] = `
exports[`<MarkdownDisplay /> > renders headers with correct levels 1`] = `
"Header 1 "Header 1
Header 2 Header 2
Header 3 Header 3
@@ -48,7 +50,7 @@ Header 4
" "
`; `;
exports[`<MarkdownDisplay /> > renders horizontal rules 1`] = ` exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders horizontal rules 1`] = `
"Hello "Hello
--- ---
World World
@@ -57,22 +59,20 @@ Test
" "
`; `;
exports[`<MarkdownDisplay /> > renders nested unordered lists 1`] = ` exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders nested unordered lists 1`] = `
" * Level 1 " * Level 1
* Level 2 * Level 2
* Level 3 * Level 3
" "
`; `;
exports[`<MarkdownDisplay /> > renders nothing for empty text 1`] = `""`; exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders ordered lists 1`] = `
exports[`<MarkdownDisplay /> > renders ordered lists 1`] = `
" 1. First item " 1. First item
2. Second item 2. Second item
" "
`; `;
exports[`<MarkdownDisplay /> > renders tables correctly 1`] = ` exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders tables correctly 1`] = `
" "
┌──────────┬──────────┐ ┌──────────┬──────────┐
│ Header 1 │ Header 2 │ │ Header 1 │ Header 2 │
@@ -83,11 +83,99 @@ exports[`<MarkdownDisplay /> > renders tables correctly 1`] = `
" "
`; `;
exports[`<MarkdownDisplay /> > renders unordered lists with different markers 1`] = ` exports[`<MarkdownDisplay /> > with 'Unix' line endings > renders unordered lists with different markers 1`] = `
" - item A " - item A
* item B * item B
+ item C + item C
" "
`; `;
exports[`<MarkdownDisplay /> > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`; exports[`<MarkdownDisplay /> > with 'Unix' line endings > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > correctly parses a mix of markdown elements 1`] = `
"Main Title
Here is a paragraph.
- List item 1
- List item 2
1 some code
Another paragraph.
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > handles a table at the end of the input 1`] = `
"Some text before.
| A | B |
|---|
| 1 | 2 |"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > inserts a single space between paragraphs 1`] = `
"Paragraph 1.
Paragraph 2."
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders a fenced code block with a language 1`] = `
" 1 const x = 1;
2 console.log(x);"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders a fenced code block without a language 1`] = `" 1 plain text"`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders headers with correct levels 1`] = `
"Header 1
Header 2
Header 3
Header 4
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders horizontal rules 1`] = `
"Hello
---
World
---
Test
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders nested unordered lists 1`] = `
" * Level 1
* Level 2
* Level 3
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders ordered lists 1`] = `
" 1. First item
2. Second item
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders tables correctly 1`] = `
"
┌──────────┬──────────┐
│ Header 1 │ Header 2 │
├──────────┼──────────┤
│ Cell 1 │ Cell 2 │
│ Cell 3 │ Cell 4 │
└──────────┴──────────┘
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > renders unordered lists with different markers 1`] = `
" - item A
* item B
+ item C
"
`;
exports[`<MarkdownDisplay /> > with 'Windows' line endings > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`;