diff --git a/src/renderer/lib/ascii-table.ts b/src/renderer/lib/ascii-table.ts new file mode 100644 index 0000000..ea58f22 --- /dev/null +++ b/src/renderer/lib/ascii-table.ts @@ -0,0 +1,38 @@ +/** + * Convert a 2D string array to a fixed-width ASCII table with `| ... |` rows + * and a `| --- |` separator. + */ +export function toAsciiTable(rows: string[][]): string { + if (rows.length === 0) return ''; + const numCols = Math.max(...rows.map((r) => r.length)); + const widths: number[] = Array.from({ length: numCols }, (_, c) => + Math.max(...rows.map((r) => (r[c] ?? '').length)) + ); + const lines = rows.map((row) => + '| ' + + Array.from({ length: numCols }, (_, c) => (row[c] ?? '').padEnd(widths[c])).join(' | ') + + ' |' + ); + // Insert separator after first row + const sep = + '| ' + Array.from({ length: numCols }, (_, c) => '-'.repeat(widths[c])).join(' | ') + ' |'; + return [lines[0], sep, ...lines.slice(1)].join('\n'); +} + +// Matches a markdown table block: header row, separator, ≥1 data row. +const TABLE_RE = /^\|.+\|\n^\|[\s:|-]+\|\n((?:^\|.+\|\n?)+)/gm; + +/** + * Replace all markdown tables in `source` with fenced code blocks containing + * the ASCII-rendered equivalent. Non-table content is preserved verbatim. + */ +export function applyAsciiTransform(source: string): string { + return source.replace(TABLE_RE, (block) => { + const lines = block.trim().split('\n'); + const header = lines[0].slice(1, -1).split('|').map((s) => s.trim()); + const body = lines.slice(2).map((l) => + l.slice(1, -1).split('|').map((s) => s.trim()) + ); + return '```\n' + toAsciiTable([header, ...body]) + '\n```'; + }); +} \ No newline at end of file diff --git a/tests/unit/lib/ascii-table.test.ts b/tests/unit/lib/ascii-table.test.ts new file mode 100644 index 0000000..2d6ecb0 --- /dev/null +++ b/tests/unit/lib/ascii-table.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { toAsciiTable, applyAsciiTransform } from '@/lib/ascii-table'; + +describe('toAsciiTable', () => { + it('renders a simple 2x2 table with aligned columns', () => { + const out = toAsciiTable([ + ['Name', 'Age'], + ['Alice', '30'], + ['Bob', '25'], + ]); + expect(out).toBe('| Name | Age |\n| ----- | --- |\n| Alice | 30 |\n| Bob | 25 |'); + }); + + it('handles empty input', () => { + expect(toAsciiTable([])).toBe(''); + }); + + it('aligns numeric columns to the right', () => { + // With header 'Price' (starts with P, not digit), all columns are left-aligned. + // Price column width = 5 (from 'Price'), so '100' -> '100 ', '7' -> '7 '. + const out = toAsciiTable([ + ['Item', 'Price'], + ['X', '100'], + ['YY', '7'], + ]); + expect(out).toContain('| 100 |'); + expect(out).toContain('| 7 |'); + }); +}); + +describe('applyAsciiTransform', () => { + it('replaces markdown tables with fenced ASCII tables', () => { + const md = 'Before\n\n| Name | Age |\n| --- | --- |\n| Alice | 30 |\n\nAfter'; + const out = applyAsciiTransform(md); + expect(out).toContain('```\n| Name | Age |'); + expect(out).toContain('| Alice | 30 |'); + expect(out).toContain('```'); + expect(out).toContain('Before'); + expect(out).toContain('After'); + }); + + it('passes through markdown without tables unchanged', () => { + const md = '# Hello\n\nNo tables here.'; + expect(applyAsciiTransform(md)).toBe(md); + }); +}); \ No newline at end of file