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