feat(tui): add markdown rendering utility
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderMarkdown } from './markdown.js';
|
||||
|
||||
describe('renderMarkdown', () => {
|
||||
it('renders plain text unchanged', () => {
|
||||
const result = renderMarkdown('Hello world');
|
||||
expect(result).toContain('Hello world');
|
||||
});
|
||||
|
||||
it('renders bold text', () => {
|
||||
const result = renderMarkdown('This is **bold** text');
|
||||
expect(result).toContain('bold');
|
||||
});
|
||||
|
||||
it('renders code blocks', () => {
|
||||
const result = renderMarkdown('```javascript\nconst x = 1;\n```');
|
||||
expect(result).toContain('const');
|
||||
expect(result).toContain('x');
|
||||
});
|
||||
|
||||
it('renders inline code', () => {
|
||||
const result = renderMarkdown('Use `console.log()` for debugging');
|
||||
expect(result).toContain('console.log()');
|
||||
});
|
||||
|
||||
it('renders lists', () => {
|
||||
const result = renderMarkdown('- Item 1\n- Item 2');
|
||||
expect(result).toContain('Item 1');
|
||||
expect(result).toContain('Item 2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { marked, Renderer, type Tokens } from 'marked';
|
||||
import { highlight } from 'cli-highlight';
|
||||
|
||||
// Create a custom renderer extending the base Renderer
|
||||
class TerminalRenderer extends Renderer {
|
||||
code({ text, lang }: Tokens.Code): string {
|
||||
try {
|
||||
return '\n' + highlight(text, { language: lang || 'plaintext' }) + '\n';
|
||||
} catch {
|
||||
return '\n' + text + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
codespan({ text }: Tokens.Codespan): string {
|
||||
return `\x1b[36m${text}\x1b[0m`; // Cyan for inline code
|
||||
}
|
||||
|
||||
strong({ tokens }: Tokens.Strong): string {
|
||||
const text = this.parser.parseInline(tokens);
|
||||
return `\x1b[1m${text}\x1b[0m`; // Bold
|
||||
}
|
||||
|
||||
em({ tokens }: Tokens.Em): string {
|
||||
const text = this.parser.parseInline(tokens);
|
||||
return `\x1b[3m${text}\x1b[0m`; // Italic
|
||||
}
|
||||
|
||||
paragraph({ tokens }: Tokens.Paragraph): string {
|
||||
return this.parser.parseInline(tokens) + '\n';
|
||||
}
|
||||
|
||||
list({ items, ordered }: Tokens.List): string {
|
||||
let body = '';
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const bullet = ordered ? `${i + 1}. ` : ' • ';
|
||||
body += bullet + this.parser.parseInline(item.tokens) + '\n';
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
listitem({ tokens }: Tokens.ListItem): string {
|
||||
return this.parser.parseInline(tokens);
|
||||
}
|
||||
|
||||
heading({ tokens }: Tokens.Heading): string {
|
||||
const text = this.parser.parseInline(tokens);
|
||||
return `\x1b[1m\x1b[4m${text}\x1b[0m\n`; // Bold + underline
|
||||
}
|
||||
|
||||
link({ tokens, href }: Tokens.Link): string {
|
||||
const text = this.parser.parseInline(tokens);
|
||||
return `\x1b[4m${text}\x1b[0m (\x1b[34m${href}\x1b[0m)`; // Underline text, blue URL
|
||||
}
|
||||
|
||||
blockquote({ tokens }: Tokens.Blockquote): string {
|
||||
const text = this.parser.parse(tokens);
|
||||
return text.split('\n').map((line: string) => ` │ ${line}`).join('\n') + '\n';
|
||||
}
|
||||
|
||||
hr(): string {
|
||||
return '─'.repeat(40) + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Configure marked with our renderer
|
||||
marked.use({ renderer: new TerminalRenderer() });
|
||||
|
||||
export function renderMarkdown(text: string): string {
|
||||
try {
|
||||
const rendered = marked.parse(text);
|
||||
// marked.parse can return string | Promise<string>, we only use sync
|
||||
if (typeof rendered === 'string') {
|
||||
return rendered.trim();
|
||||
}
|
||||
return text;
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user