obsidian-logfire/src/projection/formatters.ts
tolvitty 3c8c22ee07 Localize UI to English across all 22 source files
Translates all German user-facing strings (command names, notices,
settings, modal labels, template names/descriptions, error messages,
status bar, and code comments) to English.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 12:17:24 +01:00

180 lines
6.8 KiB
TypeScript

import { SectionConfig, BuiltinFormat } from '../types';
import { toMarkdownTable } from '../viz/table-renderer';
// ---------------------------------------------------------------------------
// Section → Markdown
// ---------------------------------------------------------------------------
export function formatSection(rows: Record<string, unknown>[], 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<string, unknown>[], 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, unknown>[]): string {
const keys = Object.keys(rows[0]);
return toMarkdownTable(keys, rows);
}
function formatAsTableWithColumns(
rows: Record<string, unknown>[],
columns: { header: string; value: string; align?: string }[],
): string {
const headers = columns.map(c => c.header);
const mappedRows = rows.map(row => {
const mapped: Record<string, unknown> = {};
for (const col of columns) {
mapped[col.header] = row[col.value] ?? '';
}
return mapped;
});
return toMarkdownTable(headers, mappedRows);
}
// ---------------------------------------------------------------------------
// Summary
// ---------------------------------------------------------------------------
function formatAsSummary(
rows: Record<string, unknown>[],
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<string, unknown>[],
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<string, unknown>[], 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<string, unknown>[], template: string): string {
return template.replace(/\{\{rows\}\}/g, JSON.stringify(rows, null, 2));
}
// ---------------------------------------------------------------------------
// Frontmatter
// ---------------------------------------------------------------------------
export function buildFrontmatter(data: Record<string, string>): 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 ?? '');
}