@sbeeredd04/auto-git
Version:
AI-powered Git automation with intelligent commit decisions using Gemini function calling, smart diff optimization, push control, and enhanced interactive terminal session with persistent command history
219 lines (187 loc) • 6.82 kB
text/typescript
/**
* Markdown rendering utilities for GitCue terminal
* Provides clean, formatted markdown rendering for terminal output
*/
export interface MarkdownRenderOptions {
maxWidth?: number;
colors?: {
header: string;
code: string;
bold: string;
italic: string;
list: string;
quote: string;
reset: string;
dim: string;
};
}
export class MarkdownRenderer {
private options: Required<MarkdownRenderOptions>;
constructor(options: MarkdownRenderOptions = {}) {
this.options = {
maxWidth: options.maxWidth || 80,
colors: {
header: '\x1b[1m\x1b[34m', // Bold blue
code: '\x1b[36m', // Cyan
bold: '\x1b[1m', // Bold
italic: '\x1b[3m', // Italic
list: '\x1b[32m', // Green
quote: '\x1b[90m', // Gray
reset: '\x1b[0m', // Reset
dim: '\x1b[2m', // Dim
...options.colors
}
};
}
/**
* Render markdown content to terminal-friendly format
*/
render(content: string): string {
const lines = content.split('\n');
const rendered: string[] = [];
let inCodeBlock = false;
let codeBlockLanguage = '';
for (let line of lines) {
// Handle code blocks
if (line.startsWith('```')) {
if (inCodeBlock) {
inCodeBlock = false;
rendered.push(this.options.colors.dim + '└─' + this.options.colors.reset);
} else {
inCodeBlock = true;
codeBlockLanguage = line.substring(3).trim();
rendered.push(this.options.colors.dim + '┌─ ' + (codeBlockLanguage || 'code') + this.options.colors.reset);
}
continue;
}
if (inCodeBlock) {
rendered.push(this.options.colors.code + '│ ' + line + this.options.colors.reset);
continue;
}
// Process the line for inline formatting
const processedLine = this.processInlineFormatting(line);
rendered.push(processedLine);
}
return rendered.join('\r\n');
}
/**
* Process inline markdown formatting
*/
private processInlineFormatting(line: string): string {
// Skip empty lines
if (line.trim() === '') {
return '';
}
// Handle headers
if (line.startsWith('####')) {
return this.options.colors.header + '▸ ' + line.substring(4).trim() + this.options.colors.reset;
}
if (line.startsWith('###')) {
return this.options.colors.header + '▶ ' + line.substring(3).trim() + this.options.colors.reset;
}
if (line.startsWith('##')) {
return this.options.colors.header + '■ ' + line.substring(2).trim() + this.options.colors.reset;
}
if (line.startsWith('#')) {
return this.options.colors.header + '● ' + line.substring(1).trim() + this.options.colors.reset;
}
// Handle lists
if (line.match(/^\s*[-*+]\s/)) {
return line.replace(/^(\s*)([-*+])(\s)/, `$1${this.options.colors.list}•${this.options.colors.reset}$3`);
}
// Handle numbered lists
if (line.match(/^\s*\d+\.\s/)) {
return line.replace(/^(\s*)(\d+\.)(\s)/, `$1${this.options.colors.list}$2${this.options.colors.reset}$3`);
}
// Handle blockquotes
if (line.startsWith('>')) {
return this.options.colors.quote + '│ ' + line.substring(1).trim() + this.options.colors.reset;
}
// Handle inline formatting
let processed = line;
// Bold text **text**
processed = processed.replace(/\*\*(.*?)\*\*/g, this.options.colors.bold + '$1' + this.options.colors.reset);
// Italic text *text*
processed = processed.replace(/\*(.*?)\*/g, this.options.colors.italic + '$1' + this.options.colors.reset);
// Inline code `code`
processed = processed.replace(/`(.*?)`/g, this.options.colors.code + '$1' + this.options.colors.reset);
return processed;
}
/**
* Create a simple box around text
*/
createBox(text: string, title?: string): string {
const lines = text.split('\n');
const maxLength = Math.max(...lines.map(line => this.stripAnsi(line).length), title ? title.length : 0);
const width = Math.min(maxLength + 4, this.options.maxWidth);
let box = this.options.colors.dim + '┌' + '─'.repeat(width - 2) + '┐' + this.options.colors.reset + '\r\n';
if (title) {
const padding = Math.max(0, width - title.length - 4);
const leftPad = Math.floor(padding / 2);
const rightPad = padding - leftPad;
box += this.options.colors.dim + '│ ' + this.options.colors.reset +
' '.repeat(leftPad) + this.options.colors.bold + title + this.options.colors.reset +
' '.repeat(rightPad) + this.options.colors.dim + ' │' + this.options.colors.reset + '\r\n';
box += this.options.colors.dim + '├' + '─'.repeat(width - 2) + '┤' + this.options.colors.reset + '\r\n';
}
lines.forEach(line => {
const cleanLine = this.stripAnsi(line);
const padding = Math.max(0, width - cleanLine.length - 4);
box += this.options.colors.dim + '│ ' + this.options.colors.reset +
line + ' '.repeat(padding) +
this.options.colors.dim + ' │' + this.options.colors.reset + '\r\n';
});
box += this.options.colors.dim + '└' + '─'.repeat(width - 2) + '┘' + this.options.colors.reset;
return box;
}
/**
* Strip ANSI escape codes from text
*/
private stripAnsi(text: string): string {
return text.replace(/\x1b\[[0-9;]*m/g, '');
}
/**
* Wrap text to specified width
*/
wrapText(text: string, width?: number): string {
const maxWidth = width || this.options.maxWidth;
const words = text.split(' ');
const lines: string[] = [];
let currentLine = '';
for (const word of words) {
if ((currentLine + word).length > maxWidth) {
if (currentLine) {
lines.push(currentLine.trim());
currentLine = word + ' ';
} else {
lines.push(word);
}
} else {
currentLine += word + ' ';
}
}
if (currentLine) {
lines.push(currentLine.trim());
}
return lines.join('\r\n');
}
}
/**
* Default markdown renderer instance
*/
export const defaultMarkdownRenderer = new MarkdownRenderer();
/**
* Quick render function for markdown content
*/
export function renderMarkdown(content: string, options?: MarkdownRenderOptions): string {
const renderer = new MarkdownRenderer(options);
return renderer.render(content);
}
/**
* Create a formatted error suggestion box
*/
export function createErrorSuggestionBox(suggestion: string): string {
const renderer = new MarkdownRenderer();
const rendered = renderer.render(suggestion);
return renderer.createBox(rendered, '�� AI Analysis');
}