import { SectionConfig, BuiltinFormat } from '../types'; import { toMarkdownTable } from '../viz/table-renderer'; // --------------------------------------------------------------------------- // Section → Markdown // --------------------------------------------------------------------------- export function formatSection(rows: Record[], section: SectionConfig): string { if (rows.length === 0) return `### ${section.heading}\n\n*No data.*\n`; const heading = `### ${section.heading}\n\n`; if (section.format.type === 'custom' && section.format.customTemplate) { return heading + applyCustomTemplate(rows, section.format.customTemplate) + '\n'; } const fmt = section.format.builtin; if (!fmt) return heading + formatAsTable(rows) + '\n'; switch (fmt.name) { case 'timeline': return heading + formatAsTimeline(rows, fmt.showTimestamp, fmt.showPayload) + '\n'; case 'table': return heading + formatAsTableWithColumns(rows, fmt.columns) + '\n'; case 'summary': return heading + formatAsSummary(rows, fmt.metrics) + '\n'; case 'metric': return heading + formatAsMetric(rows, fmt) + '\n'; case 'heatmap': return heading + formatAsHeatmap(rows, fmt.labelField, fmt.valueField) + '\n'; default: return heading + formatAsTable(rows) + '\n'; } } // --------------------------------------------------------------------------- // Timeline // --------------------------------------------------------------------------- function formatAsTimeline(rows: Record[], showTs: boolean, showPayload: boolean): string { const lines: string[] = []; for (const row of rows) { const parts: string[] = []; if (showTs && row.timestamp != null) { const ts = typeof row.timestamp === 'number' ? new Date(row.timestamp).toLocaleTimeString() : String(row.timestamp); parts.push(`**${ts}**`); } parts.push(String(row.type ?? '')); if (row.source) parts.push(`\`${row.source}\``); if (showPayload && row.payload) { const p = typeof row.payload === 'string' ? row.payload : JSON.stringify(row.payload); if (p !== '{}') parts.push(`— ${p}`); } lines.push(`- ${parts.join(' ')}`); } return lines.join('\n'); } // --------------------------------------------------------------------------- // Table // --------------------------------------------------------------------------- function formatAsTable(rows: Record[]): string { const keys = Object.keys(rows[0]); return toMarkdownTable(keys, rows); } function formatAsTableWithColumns( rows: Record[], columns: { header: string; value: string; align?: string }[], ): string { const headers = columns.map(c => c.header); const mappedRows = rows.map(row => { const mapped: Record = {}; for (const col of columns) { mapped[col.header] = row[col.value] ?? ''; } return mapped; }); return toMarkdownTable(headers, mappedRows); } // --------------------------------------------------------------------------- // Summary // --------------------------------------------------------------------------- function formatAsSummary( rows: Record[], metrics: { label: string; aggregate: string; field?: string }[], ): string { const lines: string[] = []; for (const m of metrics) { const values = rows.map(r => Number(r[m.field ?? 'count'] ?? 0)); let result: number; switch (m.aggregate) { case 'sum': result = values.reduce((a, b) => a + b, 0); break; case 'avg': result = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; break; case 'min': result = values.length > 0 ? Math.min(...values) : 0; break; case 'max': result = values.length > 0 ? Math.max(...values) : 0; break; default: result = rows.length; } lines.push(`- **${m.label}**: ${result.toLocaleString()}`); } return lines.join('\n'); } // --------------------------------------------------------------------------- // Metric // --------------------------------------------------------------------------- function formatAsMetric( rows: Record[], fmt: { aggregate: string; field?: string }, ): string { const values = rows.map(r => Number(r[fmt.field ?? Object.keys(r).pop()!] ?? 0)); let result: number; switch (fmt.aggregate) { case 'sum': result = values.reduce((a, b) => a + b, 0); break; case 'avg': result = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; break; case 'min': result = values.length > 0 ? Math.min(...values) : 0; break; case 'max': result = values.length > 0 ? Math.max(...values) : 0; break; default: result = rows.length; } return `**${result.toLocaleString()}**`; } // --------------------------------------------------------------------------- // Heatmap // --------------------------------------------------------------------------- function formatAsHeatmap(rows: Record[], labelField: string, valueField: string): string { const maxVal = Math.max(...rows.map(r => Number(r[valueField] ?? 0)), 1); const lines: string[] = []; for (const row of rows) { const label = String(row[labelField] ?? ''); const val = Number(row[valueField] ?? 0); const barLen = Math.round((val / maxVal) * 30); const bar = '\u2588'.repeat(barLen); lines.push(`\`${label.padEnd(12)}\` ${bar} ${val}`); } return lines.join('\n'); } // --------------------------------------------------------------------------- // Custom template // --------------------------------------------------------------------------- function applyCustomTemplate(rows: Record[], template: string): string { return template.replace(/\{\{rows\}\}/g, JSON.stringify(rows, null, 2)); } // --------------------------------------------------------------------------- // Frontmatter // --------------------------------------------------------------------------- export function buildFrontmatter(data: Record): string { if (Object.keys(data).length === 0) return ''; const lines = ['---']; for (const [key, value] of Object.entries(data)) { lines.push(`${key}: ${value}`); } lines.push('---', ''); return lines.join('\n'); } // --------------------------------------------------------------------------- // Date placeholders // --------------------------------------------------------------------------- export function resolvePlaceholders(pattern: string, context: { date?: Date; sessionId?: string }): string { const d = context.date ?? new Date(); return pattern .replace(/\{\{date\}\}/g, d.toISOString().substring(0, 10)) .replace(/\{\{year\}\}/g, String(d.getFullYear())) .replace(/\{\{month\}\}/g, String(d.getMonth() + 1).padStart(2, '0')) .replace(/\{\{day\}\}/g, String(d.getDate()).padStart(2, '0')) .replace(/\{\{sessionId\}\}/g, context.sessionId ?? ''); }