diff --git a/package.json b/package.json index e4610d3..0b39c20 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "license": "MIT", "devDependencies": { "@types/better-sqlite3": "^7.6.0", - "@types/marked-terminal": "^6.1.1", "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/turndown": "^5.0.0", @@ -56,7 +55,6 @@ "ink-text-input": "^6.0.0", "linkedom": "^0.18.0", "marked": "^17.0.1", - "marked-terminal": "^7.3.0", "ollama": "^0.5.0", "openai": "^4.0.0", "puppeteer-core": "^24.37.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69e812b..fa57b35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,9 +59,6 @@ importers: marked: specifier: ^17.0.1 version: 17.0.1 - marked-terminal: - specifier: ^7.3.0 - version: 7.3.0(marked@17.0.1) ollama: specifier: ^0.5.0 version: 0.5.18 @@ -93,9 +90,6 @@ importers: '@types/better-sqlite3': specifier: ^7.6.0 version: 7.6.13 - '@types/marked-terminal': - specifier: ^6.1.1 - version: 6.1.1 '@types/node': specifier: ^22.0.0 version: 22.19.7 @@ -275,10 +269,6 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - '@discordjs/builders@1.13.1': resolution: {integrity: sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==} engines: {node: '>=16.11.0'} @@ -708,10 +698,6 @@ packages: resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - '@sindresorhus/is@4.6.0': - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - '@slack/bolt@4.6.0': resolution: {integrity: sha512-xPgfUs2+OXSugz54Ky07pA890+Qydk22SYToi8uGpXeHSt1JWwFJkRyd/9Vlg5I1AdfdpGXExDpwnbuN9Q/2dQ==} engines: {node: '>=18', npm: '>=8.6.0'} @@ -939,9 +925,6 @@ packages: '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/cardinal@2.1.1': - resolution: {integrity: sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -969,9 +952,6 @@ packages: '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - '@types/marked-terminal@6.1.1': - resolution: {integrity: sha512-DfoUqkmFDCED7eBY9vFUhJ9fW8oZcMAK5EwRDQ9drjTbpQa+DnBTQQCwWhTFVf4WsZ6yYcJTI8D91wxTWXRZZQ==} - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -1292,10 +1272,6 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -1321,10 +1297,6 @@ packages: engines: {node: '>=8.0.0', npm: '>=5.0.0'} hasBin: true - cli-table3@0.6.5: - resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} - engines: {node: 10.* || >= 12.*} - cli-truncate@5.1.1: resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} engines: {node: '>=20'} @@ -1514,9 +1486,6 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emojilib@2.4.0: - resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -2158,17 +2127,6 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - marked-terminal@7.3.0: - resolution: {integrity: sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==} - engines: {node: '>=16.0.0'} - peerDependencies: - marked: '>=1 <16' - - marked@11.2.0: - resolution: {integrity: sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==} - engines: {node: '>= 18'} - hasBin: true - marked@17.0.1: resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==} engines: {node: '>= 20'} @@ -2269,10 +2227,6 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead - node-emoji@2.2.0: - resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} - engines: {node: '>=18'} - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -2610,10 +2564,6 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - skin-tone@2.0.0: - resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} - engines: {node: '>=8'} - slice-ansi@7.1.2: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} @@ -2699,10 +2649,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-hyperlinks@3.2.0: - resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} - engines: {node: '>=14.18'} - tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -2812,10 +2758,6 @@ packages: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} - unicode-emoji-modifier-base@1.0.0: - resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} - engines: {node: '>=4'} - universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -3473,9 +3415,6 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} - '@colors/colors@1.5.0': - optional: true - '@discordjs/builders@1.13.1': dependencies: '@discordjs/formatters': 0.6.2 @@ -3797,8 +3736,6 @@ snapshots: '@sapphire/snowflake@3.5.3': {} - '@sindresorhus/is@4.6.0': {} - '@slack/bolt@4.6.0(@types/express@5.0.6)': dependencies: '@slack/logger': 4.0.0 @@ -4179,8 +4116,6 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.19.7 - '@types/cardinal@2.1.1': {} - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -4216,13 +4151,6 @@ snapshots: '@types/ms': 2.1.0 '@types/node': 22.19.7 - '@types/marked-terminal@6.1.1': - dependencies: - '@types/cardinal': 2.1.1 - '@types/node': 22.19.7 - chalk: 5.6.2 - marked: 11.2.0 - '@types/ms@2.1.0': {} '@types/node-fetch@2.6.13': @@ -4584,8 +4512,6 @@ snapshots: chalk@5.6.2: {} - char-regex@1.0.2: {} - check-error@2.1.3: {} chownr@1.1.4: {} @@ -4611,12 +4537,6 @@ snapshots: parse5-htmlparser2-tree-adapter: 6.0.1 yargs: 16.2.0 - cli-table3@0.6.5: - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - cli-truncate@5.1.1: dependencies: slice-ansi: 7.1.2 @@ -4808,8 +4728,6 @@ snapshots: emoji-regex@8.0.0: {} - emojilib@2.4.0: {} - encodeurl@2.0.0: {} end-of-stream@1.4.5: @@ -5551,19 +5469,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - marked-terminal@7.3.0(marked@17.0.1): - dependencies: - ansi-escapes: 7.3.0 - ansi-regex: 6.2.2 - chalk: 5.6.2 - cli-highlight: 2.1.11 - cli-table3: 0.6.5 - marked: 17.0.1 - node-emoji: 2.2.0 - supports-hyperlinks: 3.2.0 - - marked@11.2.0: {} - marked@17.0.1: {} math-intrinsics@1.1.0: {} @@ -5634,13 +5539,6 @@ snapshots: node-domexception@1.0.0: {} - node-emoji@2.2.0: - dependencies: - '@sindresorhus/is': 4.6.0 - char-regex: 1.0.2 - emojilib: 2.4.0 - skin-tone: 2.0.0 - node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -6070,10 +5968,6 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 - skin-tone@2.0.0: - dependencies: - unicode-emoji-modifier-base: 1.0.0 - slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 @@ -6166,11 +6060,6 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-hyperlinks@3.2.0: - dependencies: - has-flag: 4.0.0 - supports-color: 7.2.0 - tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -6288,8 +6177,6 @@ snapshots: undici@6.21.3: {} - unicode-emoji-modifier-base@1.0.0: {} - universalify@2.0.1: optional: true diff --git a/src/frontends/tui/markdown.test.ts b/src/frontends/tui/markdown.test.ts index ec1ca0e..bc1b9a6 100644 --- a/src/frontends/tui/markdown.test.ts +++ b/src/frontends/tui/markdown.test.ts @@ -7,8 +7,37 @@ describe('renderMarkdown', () => { expect(result).toContain('Hello world'); }); - it('renders bold text', () => { + it('renders bold text with ANSI bold code', () => { const result = renderMarkdown('This is **bold** text'); + expect(result).toContain('\x1b[1m'); + expect(result).toContain('bold'); + expect(result).toContain('\x1b[0m'); + }); + + it('renders italic text with ANSI italic code', () => { + const result = renderMarkdown('This is *italic* text'); + expect(result).toContain('\x1b[3m'); + expect(result).toContain('italic'); + expect(result).toContain('\x1b[0m'); + }); + + it('renders inline code with ANSI cyan code', () => { + const result = renderMarkdown('Use `console.log()` for debugging'); + expect(result).toContain('\x1b[36m'); + expect(result).toContain('console.log()'); + expect(result).toContain('\x1b[0m'); + }); + + it('renders headings with bold + underline', () => { + const result = renderMarkdown('# My Heading'); + expect(result).toContain('\x1b[1m\x1b[4m'); + expect(result).toContain('My Heading'); + expect(result).toContain('\x1b[0m'); + }); + + it('renders heading with inline formatting', () => { + const result = renderMarkdown('## A **bold** heading'); + expect(result).toContain('\x1b[1m'); expect(result).toContain('bold'); }); @@ -18,14 +47,66 @@ describe('renderMarkdown', () => { expect(result).toContain('x'); }); - it('renders inline code', () => { - const result = renderMarkdown('Use `console.log()` for debugging'); - expect(result).toContain('console.log()'); + it('renders code blocks with unknown language without crashing', () => { + const result = renderMarkdown('```madeuplang\nsome code\n```'); + expect(result).toContain('some code'); }); - it('renders lists', () => { + it('renders unordered lists', () => { const result = renderMarkdown('- Item 1\n- Item 2'); + expect(result).toContain('•'); expect(result).toContain('Item 1'); expect(result).toContain('Item 2'); }); + + it('renders inline formatting inside list items', () => { + const result = renderMarkdown('- **bold** item\n- `code` item'); + expect(result).toContain('\x1b[1m'); + expect(result).toContain('bold'); + expect(result).toContain('\x1b[36m'); + expect(result).toContain('code'); + }); + + it('renders blockquotes with │ prefix', () => { + const result = renderMarkdown('> This is a quote'); + expect(result).toContain('│'); + expect(result).toContain('This is a quote'); + }); + + it('renders inline formatting inside blockquotes', () => { + const result = renderMarkdown('> A **bold** quote'); + expect(result).toContain('│'); + expect(result).toContain('\x1b[1m'); + expect(result).toContain('bold'); + }); + + it('renders tables with │ separator', () => { + const result = renderMarkdown('| Col A | Col B |\n|-------|-------|\n| 1 | 2 |\n| 3 | 4 |'); + expect(result).toContain('│'); + expect(result).toContain('Col A'); + expect(result).toContain('Col B'); + expect(result).toContain('1'); + expect(result).toContain('4'); + }); + + it('renders table headers in bold', () => { + const result = renderMarkdown('| Name | Value |\n|------|-------|\n| a | b |'); + // Header cells should have bold ANSI + const lines = result.split('\n'); + const headerLine = lines[0]; + expect(headerLine).toContain('\x1b[1m'); + }); + + it('renders horizontal rules', () => { + const result = renderMarkdown('---'); + expect(result).toContain('─'); + }); + + it('renders links with underline and blue URL', () => { + const result = renderMarkdown('[click here](https://example.com)'); + expect(result).toContain('\x1b[4m'); + expect(result).toContain('click here'); + expect(result).toContain('\x1b[34m'); + expect(result).toContain('https://example.com'); + }); }); diff --git a/src/frontends/tui/markdown.ts b/src/frontends/tui/markdown.ts index 6a00d9d..3c5c913 100644 --- a/src/frontends/tui/markdown.ts +++ b/src/frontends/tui/markdown.ts @@ -38,6 +38,8 @@ function convertHtmlToTerminal(text: string): string { } // Custom renderer as an object for marked.use() +// Methods that contain inline children use this.parser.parseInline(tokens) +// Methods that contain block children use this.parser.parse(tokens) const terminalRenderer: RendererObject = { code({ text, lang }: Tokens.Code): string { try { @@ -59,34 +61,85 @@ const terminalRenderer: RendererObject = { return `\x1b[3m${text}\x1b[0m`; // Italic }, - paragraph({ text }: Tokens.Paragraph): string { - return text + '\n'; + paragraph({ tokens }: Tokens.Paragraph): string { + return this.parser.parseInline(tokens) + '\n'; }, - list({ items, ordered }: Tokens.List): string { + list({ items, ordered, start }: Tokens.List): string { let body = ''; for (let i = 0; i < items.length; i++) { const item = items[i]; - const bullet = ordered ? `${i + 1}. ` : ' • '; - body += bullet + item.text + '\n'; + const bullet = ordered ? `${(start || 1) + i}. ` : ' • '; + const content = this.parser.parse(item.tokens).replace(/\n$/, ''); + body += bullet + content + '\n'; } return body; }, - listitem({ text }: Tokens.ListItem): string { - return text; + listitem({ tokens }: Tokens.ListItem): string { + return this.parser.parse(tokens); }, - heading({ text }: Tokens.Heading): string { - return `\x1b[1m\x1b[4m${text}\x1b[0m\n`; // Bold + underline + heading({ tokens, depth }: Tokens.Heading): string { + const text = this.parser.parseInline(tokens); + const prefix = depth <= 2 ? '' : '#'.repeat(depth) + ' '; + return `\x1b[1m\x1b[4m${prefix}${text}\x1b[0m\n`; // Bold + underline }, link({ text, href }: Tokens.Link): string { return `\x1b[4m${text}\x1b[0m (\x1b[34m${href}\x1b[0m)`; // Underline text, blue URL }, - blockquote({ text }: Tokens.Blockquote): string { - return text.split('\n').map((line: string) => ` │ ${line}`).join('\n') + '\n'; + blockquote({ tokens }: Tokens.Blockquote): string { + const body = this.parser.parse(tokens); + return body.split('\n').map((line: string) => ` │ ${line}`).join('\n') + '\n'; + }, + + table({ header, rows, align }: Tokens.Table): string { + // Render cell contents + const headerTexts = header.map((cell: Tokens.TableCell) => + this.parser.parseInline(cell.tokens) + ); + const rowTexts = rows.map((row: Tokens.TableCell[]) => + row.map((cell: Tokens.TableCell) => this.parser.parseInline(cell.tokens)) + ); + + // Calculate column widths (strip ANSI for measurement) + const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, ''); + const colWidths = headerTexts.map((h: string, i: number) => { + const cellWidths = rowTexts.map((row: string[]) => stripAnsi(row[i] || '').length); + return Math.max(stripAnsi(h).length, ...cellWidths); + }); + + // Pad cell to width respecting ANSI codes + const pad = (text: string, width: number, alignment: string | null) => { + const visible = stripAnsi(text).length; + const diff = width - visible; + if (diff <= 0) return text; + if (alignment === 'right') return ' '.repeat(diff) + text; + if (alignment === 'center') { + const left = Math.floor(diff / 2); + return ' '.repeat(left) + text + ' '.repeat(diff - left); + } + return text + ' '.repeat(diff); + }; + + // Build header row + const headerRow = ' ' + headerTexts.map((h: string, i: number) => + `\x1b[1m${pad(h, colWidths[i], align[i])}\x1b[0m` + ).join(' │ '); + + // Build separator + const separator = '─' + colWidths.map((w: number) => '─'.repeat(w)).join('─┼─') + '─'; + + // Build data rows + const dataRows = rowTexts.map((row: string[]) => + ' ' + row.map((cell: string, i: number) => + pad(cell, colWidths[i], align[i]) + ).join(' │ ') + ); + + return [headerRow, separator, ...dataRows].join('\n') + '\n'; }, hr(): string {