From 94f43c79d0edf980294bdd700f8bffbc6cac1c69 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Mon, 29 Sep 2025 13:38:27 -0700 Subject: [PATCH] Fix markdown rendering on Windows (#10185) --- .../cli/src/ui/utils/MarkdownDisplay.test.tsx | 256 +++++++++--------- packages/cli/src/ui/utils/MarkdownDisplay.tsx | 3 +- .../MarkdownDisplay.test.tsx.snap | 124 +++++++-- 3 files changed, 238 insertions(+), 145 deletions(-) diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx index 634888b07c..927a69e154 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx @@ -7,7 +7,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { MarkdownDisplay } from './MarkdownDisplay.js'; import { LoadedSettings } from '../../config/settings.js'; -import { EOL } from 'node:os'; import { renderWithProviders } from '../../test-utils/render.js'; describe('', () => { @@ -36,132 +35,138 @@ describe('', () => { expect(lastFrame()).toMatchSnapshot(); }); - it('renders headers with correct levels', () => { - const text = ` + const lineEndings = [ + { 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 2 ### Header 3 #### Header 4 -`.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); +`.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('renders a fenced code block with a language', () => { - const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```'.replace( - /\n/g, - EOL, - ); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); + it('renders a fenced code block with a language', () => { + const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```'.replace( + /\n/g, + eol, + ); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('renders a fenced code block without a language', () => { - const text = '```\nplain text\n```'.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); + it('renders a fenced code block without a language', () => { + const text = '```\nplain text\n```'.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('handles unclosed (pending) code blocks', () => { - const text = '```typescript\nlet y = 2;'.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); + it('handles unclosed (pending) code blocks', () => { + const text = '```typescript\nlet y = 2;'.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('renders unordered lists with different markers', () => { - const text = ` + it('renders unordered lists with different markers', () => { + const text = ` - item A * item B + item C -`.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); +`.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('renders nested unordered lists', () => { - const text = ` + it('renders nested unordered lists', () => { + const text = ` * Level 1 * Level 2 * Level 3 -`.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); +`.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('renders ordered lists', () => { - const text = ` + it('renders ordered lists', () => { + const text = ` 1. First item 2. Second item -`.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); +`.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('renders horizontal rules', () => { - const text = ` + it('renders horizontal rules', () => { + const text = ` Hello --- World *** Test -`.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); +`.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('renders tables correctly', () => { - const text = ` + it('renders tables correctly', () => { + const text = ` | Header 1 | Header 2 | |----------|:--------:| | Cell 1 | Cell 2 | | Cell 3 | Cell 4 | -`.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); +`.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('handles a table at the end of the input', () => { - const text = ` + it('handles a table at the end of the input', () => { + const text = ` Some text before. | A | B | |---| -| 1 | 2 |`.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); +| 1 | 2 |`.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('inserts a single space between paragraphs', () => { - const text = `Paragraph 1. + it('inserts a single space between paragraphs', () => { + const text = `Paragraph 1. -Paragraph 2.`.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); +Paragraph 2.`.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('correctly parses a mix of markdown elements', () => { - const text = ` + it('correctly parses a mix of markdown elements', () => { + const text = ` # Main Title Here is a paragraph. @@ -174,42 +179,43 @@ some code \`\`\` Another paragraph. -`.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); +`.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); - it('hides line numbers in code blocks when showLineNumbers is false', () => { - const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, EOL); - const settings = new LoadedSettings( - { path: '', settings: {}, originalSettings: {} }, - { path: '', settings: {}, originalSettings: {} }, - { - path: '', - settings: { ui: { showLineNumbers: false } }, - originalSettings: { ui: { showLineNumbers: false } }, - }, - { path: '', settings: {}, originalSettings: {} }, - true, - new Set(), - ); + it('hides line numbers in code blocks when showLineNumbers is false', () => { + const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, eol); + const settings = new LoadedSettings( + { path: '', settings: {}, originalSettings: {} }, + { path: '', settings: {}, originalSettings: {} }, + { + path: '', + settings: { ui: { showLineNumbers: false } }, + originalSettings: { ui: { showLineNumbers: false } }, + }, + { path: '', settings: {}, originalSettings: {} }, + true, + new Set(), + ); - const { lastFrame } = renderWithProviders( - , - { settings }, - ); - expect(lastFrame()).toMatchSnapshot(); - expect(lastFrame()).not.toContain(' 1 '); - }); + const { lastFrame } = renderWithProviders( + , + { settings }, + ); + expect(lastFrame()).toMatchSnapshot(); + expect(lastFrame()).not.toContain(' 1 '); + }); - it('shows line numbers in code blocks by default', () => { - const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, EOL); - const { lastFrame } = renderWithProviders( - , - ); - expect(lastFrame()).toMatchSnapshot(); - expect(lastFrame()).toContain(' 1 '); + it('shows line numbers in code blocks by default', () => { + const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, eol); + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + expect(lastFrame()).toContain(' 1 '); + }); }); }); diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx index 2baea99884..da6bf21aaf 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { Text, Box } from 'ink'; -import { EOL } from 'node:os'; import { theme } from '../semantic-colors.js'; import { colorizeCode } from './CodeColorizer.js'; import { TableRenderer } from './TableRenderer.js'; @@ -35,7 +34,7 @@ const MarkdownDisplayInternal: React.FC = ({ }) => { if (!text) return <>; - const lines = text.split(EOL); + const lines = text.split(/\r?\n/); const headerRegex = /^ *(#{1,4}) +(.*)/; const codeFenceRegex = /^ *(`{3,}|~{3,}) *(\w*?) *$/; const ulItemRegex = /^([ \t]*)([-*+]) +(.*)/; diff --git a/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap index 223c293b19..c340dc8f61 100644 --- a/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap +++ b/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap @@ -1,6 +1,10 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[` > correctly parses a mix of markdown elements 1`] = ` +exports[` > renders a simple paragraph 1`] = `"Hello, world."`; + +exports[` > renders nothing for empty text 1`] = `""`; + +exports[` > with 'Unix' line endings > correctly parses a mix of markdown elements 1`] = ` "Main Title Here is a paragraph. @@ -14,33 +18,31 @@ Another paragraph. " `; -exports[` > handles a table at the end of the input 1`] = ` +exports[` > with 'Unix' line endings > handles a table at the end of the input 1`] = ` "Some text before. | A | B | |---| | 1 | 2 |" `; -exports[` > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`; +exports[` > with 'Unix' line endings > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`; -exports[` > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`; +exports[` > with 'Unix' line endings > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`; -exports[` > inserts a single space between paragraphs 1`] = ` +exports[` > with 'Unix' line endings > inserts a single space between paragraphs 1`] = ` "Paragraph 1. Paragraph 2." `; -exports[` > renders a fenced code block with a language 1`] = ` +exports[` > with 'Unix' line endings > renders a fenced code block with a language 1`] = ` " 1 const x = 1; 2 console.log(x);" `; -exports[` > renders a fenced code block without a language 1`] = `" 1 plain text"`; +exports[` > with 'Unix' line endings > renders a fenced code block without a language 1`] = `" 1 plain text"`; -exports[` > renders a simple paragraph 1`] = `"Hello, world."`; - -exports[` > renders headers with correct levels 1`] = ` +exports[` > with 'Unix' line endings > renders headers with correct levels 1`] = ` "Header 1 Header 2 Header 3 @@ -48,7 +50,7 @@ Header 4 " `; -exports[` > renders horizontal rules 1`] = ` +exports[` > with 'Unix' line endings > renders horizontal rules 1`] = ` "Hello --- World @@ -57,22 +59,20 @@ Test " `; -exports[` > renders nested unordered lists 1`] = ` +exports[` > with 'Unix' line endings > renders nested unordered lists 1`] = ` " * Level 1 * Level 2 * Level 3 " `; -exports[` > renders nothing for empty text 1`] = `""`; - -exports[` > renders ordered lists 1`] = ` +exports[` > with 'Unix' line endings > renders ordered lists 1`] = ` " 1. First item 2. Second item " `; -exports[` > renders tables correctly 1`] = ` +exports[` > with 'Unix' line endings > renders tables correctly 1`] = ` " ┌──────────┬──────────┐ │ Header 1 │ Header 2 │ @@ -83,11 +83,99 @@ exports[` > renders tables correctly 1`] = ` " `; -exports[` > renders unordered lists with different markers 1`] = ` +exports[` > with 'Unix' line endings > renders unordered lists with different markers 1`] = ` " - item A * item B + item C " `; -exports[` > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`; +exports[` > with 'Unix' line endings > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`; + +exports[` > 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[` > with 'Windows' line endings > handles a table at the end of the input 1`] = ` +"Some text before. +| A | B | +|---| +| 1 | 2 |" +`; + +exports[` > with 'Windows' line endings > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`; + +exports[` > with 'Windows' line endings > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`; + +exports[` > with 'Windows' line endings > inserts a single space between paragraphs 1`] = ` +"Paragraph 1. + +Paragraph 2." +`; + +exports[` > with 'Windows' line endings > renders a fenced code block with a language 1`] = ` +" 1 const x = 1; + 2 console.log(x);" +`; + +exports[` > with 'Windows' line endings > renders a fenced code block without a language 1`] = `" 1 plain text"`; + +exports[` > with 'Windows' line endings > renders headers with correct levels 1`] = ` +"Header 1 +Header 2 +Header 3 +Header 4 +" +`; + +exports[` > with 'Windows' line endings > renders horizontal rules 1`] = ` +"Hello +--- +World +--- +Test +" +`; + +exports[` > with 'Windows' line endings > renders nested unordered lists 1`] = ` +" * Level 1 + * Level 2 + * Level 3 +" +`; + +exports[` > with 'Windows' line endings > renders ordered lists 1`] = ` +" 1. First item + 2. Second item +" +`; + +exports[` > with 'Windows' line endings > renders tables correctly 1`] = ` +" +┌──────────┬──────────┐ +│ Header 1 │ Header 2 │ +├──────────┼──────────┤ +│ Cell 1 │ Cell 2 │ +│ Cell 3 │ Cell 4 │ +└──────────┴──────────┘ +" +`; + +exports[` > with 'Windows' line endings > renders unordered lists with different markers 1`] = ` +" - item A + * item B + + item C +" +`; + +exports[` > with 'Windows' line endings > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`;