Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions src/templates/blog/benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import type { SatoriElement, TemplateDefinition } from '../types';
import { autoFontSize, commonFields, isLightColor, mixColors, truncate } from '../utils';

type BenchmarkRow = { name: string; value: number };

const template: TemplateDefinition = {
id: 'blog-benchmark',
name: 'Benchmark',
category: 'blog',
description: 'Compare performance or size measurements with scaled horizontal bars',
tags: ['blog', 'benchmark', 'performance', 'comparison', 'stats'],
fields: [
commonFields.title,
commonFields.description,
{ key: 'name1', label: 'Name 1', type: 'text', defaultValue: 'Bun', placeholder: 'Bun', group: 'Content' },
{ key: 'value1', label: 'Value 1', type: 'number', defaultValue: 112000, placeholder: '112000', min: 0, max: 1_000_000_000, group: 'Content' },
{ key: 'name2', label: 'Name 2', type: 'text', defaultValue: 'Deno', placeholder: 'Deno', group: 'Content' },
{ key: 'value2', label: 'Value 2', type: 'number', defaultValue: 71000, placeholder: '71000', min: 0, max: 1_000_000_000, group: 'Content' },
{ key: 'name3', label: 'Name 3', type: 'text', defaultValue: 'Node', placeholder: 'Node', group: 'Content' },
{ key: 'value3', label: 'Value 3', type: 'number', defaultValue: 48000, placeholder: '48000', min: 0, max: 1_000_000_000, group: 'Content' },
{ key: 'name4', label: 'Name 4', type: 'text', defaultValue: '', placeholder: 'Optional comparison', group: 'Content' },
{ key: 'value4', label: 'Value 4', type: 'number', defaultValue: '', placeholder: 'Optional value', min: 0, max: 1_000_000_000, group: 'Content' },
{ key: 'unit', label: 'Unit', type: 'text', defaultValue: 'req/s', placeholder: 'req/s', group: 'Content' },
commonFields.siteName,
commonFields.bgColor,
commonFields.accentColor,
commonFields.textColor,
],
defaults: {
title: 'Bun vs Deno vs Node: HTTP throughput',
description: 'Requests per second, higher is better',
name1: 'Bun', value1: 112000,
name2: 'Deno', value2: 71000,
name3: 'Node', value3: 48000,
name4: '', value4: '',
unit: 'req/s',
siteName: 'codercops.com',
bgColor: '#0a0a0a',
accentColor: '#E07A5F',
textColor: '#FFFFFF',
},
render: (params) => {
const rawTitle = String(params.title ?? template.defaults.title).replace(/\s+/g, ' ').trim();
const title = truncate(rawTitle || String(template.defaults.title), 120);
const titleSize = autoFontSize(title, [
{ maxLen: 36, size: 46 },
{ maxLen: 60, size: 40 },
{ maxLen: 90, size: 34 },
{ maxLen: 120, size: 28 },
]);
const rawDescription = String(params.description ?? '').replace(/\s+/g, ' ').trim();
const description = truncate(rawDescription, 120);
const descriptionSize = autoFontSize(description, [
{ maxLen: 40, size: 20 },
{ maxLen: 75, size: 18 },
{ maxLen: 120, size: 15 },
]);
const rows: BenchmarkRow[] = [1, 2, 3, 4].flatMap((index) => {
const name = String(params[`name${index}`] ?? '').replace(/\s+/g, ' ').trim();
const rawValue = params[`value${index}`];
if (!name || rawValue === undefined || rawValue === null || String(rawValue).trim() === '') return [];

const value = Number(rawValue);
return Number.isFinite(value) && value >= 0 ? [{ name: truncate(name, 80), value }] : [];
});
const maxValue = rows.reduce((max, row) => Math.max(max, row.value), 0);
const largestIndex = rows.findIndex(({ value }) => value === maxValue);
const unit = String(params.unit ?? 'req/s').replace(/\s+/g, ' ').trim();
const valueLabels = rows.map(({ value }) => {
const formattedValue = new Intl.NumberFormat('en-US', { maximumFractionDigits: 3 }).format(value);
return `${formattedValue}${unit ? ` ${unit}` : ''}`;
});
const longestValueLabel = valueLabels.reduce((longest, label) =>
label.length > longest.length ? label : longest, '');
const displayLongestValueLabel = truncate(longestValueLabel, 36);
const valueSize = autoFontSize(displayLongestValueLabel, [
{ maxLen: 18, size: 20 },
{ maxLen: 26, size: 17 },
{ maxLen: 36, size: 14 },
]);
const valueColumnWidth = Math.min(360, Math.max(220, Math.ceil(displayLongestValueLabel.length * valueSize * 0.9 + 12)));
const siteName = String(params.siteName ?? '').replace(/\s+/g, ' ').trim();
const bgColor = String(params.bgColor || '#0a0a0a');
const accentColor = String(params.accentColor || '#E07A5F');
const textColor = String(params.textColor || '#FFFFFF');
const lightBg = isLightColor(bgColor);
const accentInk = lightBg ? mixColors(accentColor, '#000000', 0.45) : accentColor;
const foregroundColor = lightBg && isLightColor(textColor) ? '#111111' : textColor;
const mutedText = mixColors(bgColor, foregroundColor, lightBg ? 0.68 : 0.62);
const trackColor = mixColors(bgColor, foregroundColor, lightBg ? 0.13 : 0.2);

const rowElements: SatoriElement[] = rows.map(({ name, value }, index) => {
const nameSize = autoFontSize(name, [
{ maxLen: 16, size: 22 },
{ maxLen: 28, size: 19 },
{ maxLen: 48, size: 16 },
{ maxLen: 80, size: 14 },
]);
const fillColor = index === largestIndex ? accentInk : mixColors(bgColor, foregroundColor, lightBg ? 0.3 : 0.36);
const width = maxValue > 0 ? (value / maxValue) * 100 : 0;
const displayValue = truncate(valueLabels[index], 36);

return {
type: 'div',
props: {
style: { display: 'flex', alignItems: 'center', gap: '22px', width: '100%', minHeight: '68px' },
children: [
{
type: 'span',
props: {
style: {
display: 'flex', flexShrink: 0, width: '190px', minWidth: 0,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
fontSize: `${nameSize}px`, fontWeight: 600, color: foregroundColor,
},
children: name,
},
},
{
type: 'div',
props: {
style: { display: 'flex', flex: 1, minWidth: 0, height: '16px', overflow: 'hidden', borderRadius: '999px', backgroundColor: trackColor },
children: [
{
type: 'div',
props: {
style: { display: 'flex', width: `${width}%`, height: '100%', borderRadius: '999px', backgroundColor: fillColor },
children: [],
},
},
],
},
},
{
type: 'span',
props: {
style: {
display: 'flex', justifyContent: 'flex-end', flexShrink: 0, width: `${valueColumnWidth}px`, minWidth: 0,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
fontSize: `${valueSize}px`, fontWeight: 700, color: foregroundColor,
},
children: displayValue,
},
},
],
},
};
});

return {
type: 'div',
props: {
style: {
display: 'flex', flexDirection: 'column', width: '1200px', height: '630px',
boxSizing: 'border-box', padding: '52px 64px 42px', backgroundColor: bgColor,
fontFamily: 'Inter, sans-serif',
},
children: [
{
type: 'div',
props: {
style: { display: 'flex', alignSelf: 'flex-start', padding: '8px 16px', marginBottom: '18px', borderRadius: '999px', backgroundColor: mixColors(bgColor, accentInk, lightBg ? 0.08 : 0.17), border: `1px solid ${accentInk}66` },
children: {
type: 'span',
props: { style: { display: 'flex', color: accentInk, fontSize: '14px', fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase' }, children: 'Benchmark' },
},
},
},
{
type: 'h1',
props: {
style: { display: 'flex', width: '100%', margin: 0, color: foregroundColor, fontSize: `${titleSize}px`, fontWeight: 700, lineHeight: 1.18, letterSpacing: '-0.02em', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
children: title,
},
},
...(description ? [{
type: 'span',
props: {
style: { display: 'flex', marginTop: '10px', color: mutedText, fontSize: `${descriptionSize}px`, lineHeight: 1.3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
children: description,
},
}] : []),
...(rowElements.length ? [{
type: 'div',
props: {
style: { display: 'flex', flexDirection: 'column', width: '100%', gap: '14px', marginTop: 'auto', marginBottom: 'auto' },
children: rowElements,
},
}] : []),
...(siteName ? [{
type: 'div',
props: {
style: { display: 'flex', justifyContent: 'flex-end', marginTop: 'auto', paddingTop: '14px', borderTop: `1px solid ${trackColor}` },
children: { type: 'span', props: { style: { display: 'flex', fontSize: '16px', fontWeight: 600, color: mutedText }, children: truncate(siteName, 48) } },
},
}] : []),
],
},
};
},
};

export default template;
1 change: 1 addition & 0 deletions src/templates/blog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export { default as blogRetroTerminal } from './retro-terminal';
export { default as blogSeriesPart } from './series-part';
export { default as blogSummaryCard } from './summary-card';
export { default as blogTableOfContents } from './table-of-contents';
export { default as blogBenchmark } from './benchmark';
export { default as blogUpdatedPost } from './updated-post';
2 changes: 2 additions & 0 deletions src/templates/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import blogRetroTerminal from './blog/retro-terminal';
import blogSeriesPart from './blog/series-part';
import blogSummaryCard from './blog/summary-card';
import blogTableOfContents from './blog/table-of-contents';
import blogBenchmark from './blog/benchmark';
import blogUpdatedPost from './blog/updated-post';

[
Expand All @@ -75,6 +76,7 @@ import blogUpdatedPost from './blog/updated-post';
blogSeriesPart,
blogSummaryCard,
blogTableOfContents,
blogBenchmark,
blogUpdatedPost,
].forEach(registerTemplate);

Expand Down
170 changes: 170 additions & 0 deletions tests/templates/benchmark.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { describe, expect, it } from 'vitest';
import benchmark from '@/templates/blog/benchmark';
import { blogBenchmark } from '@/templates/blog';
import { getTemplate, getTemplatesByCategory } from '@/templates/registry';
import { mixColors } from '@/templates/utils';
import type { SatoriElement } from '@/templates/types';

function elements(node: unknown): SatoriElement[] {
if (Array.isArray(node)) return node.flatMap(elements);
if (!node || typeof node !== 'object' || !('type' in node)) return [];

const element = node as SatoriElement;
return [element, ...elements(element.props.children)];
}

function textContent(node: unknown): string[] {
if (typeof node === 'string') return [node];
if (Array.isArray(node)) return node.flatMap(textContent);
if (node && typeof node === 'object' && 'props' in node) {
return textContent((node as SatoriElement).props.children);
}
return [];
}

const render = (overrides: Record<string, unknown> = {}) =>
benchmark.render({ ...benchmark.defaults, ...overrides });
const rows = (tree: unknown) => elements(tree).filter((element) => element.type === 'div' && element.props.style?.minHeight === '68px');

describe('blog-benchmark', () => {
it('is re-exported and registered once in the Blog category', () => {
expect(blogBenchmark).toBe(benchmark);
expect(getTemplate('blog-benchmark')).toBe(benchmark);
expect(getTemplatesByCategory('blog').filter(({ id }) => id === 'blog-benchmark')).toHaveLength(1);
});

it('renders the label, title, optional note, ordered rows, and formatted values', () => {
const tree = render();
const text = textContent(tree);

expect(text).toContain('Benchmark');
expect(text).toContain('Bun vs Deno vs Node: HTTP throughput');
expect(text).toContain('Requests per second, higher is better');
expect(text).toContain('112,000 req/s');
expect(text).toContain('71,000 req/s');
expect(text).toContain('48,000 req/s');
expect(text).toContain('codercops.com');
expect(rows(tree)).toHaveLength(3);
expect(rows(tree).map((row) => textContent((row.props.children as SatoriElement[])[0])[0]))
.toEqual(['Bun', 'Deno', 'Node']);
});

it('scales every bar to the largest value and highlights only the first largest row', () => {
const tree = render({ value1: '500', value2: '250', value3: '125' });
const rowElements = rows(tree);
const fills = rowElements.map((row) => (row.props.children as SatoriElement[])[1].props.children[0] as SatoriElement);

expect(fills.map(({ props }) => props.style.width)).toEqual(['100%', '50%', '25%']);
expect(fills[0].props.style.backgroundColor).toBe('#E07A5F');
expect(fills[1].props.style.backgroundColor).not.toBe('#E07A5F');
expect(fills[2].props.style.backgroundColor).not.toBe('#E07A5F');
});

it('skips blank names and invalid, negative, or blank values without gaps', () => {
const tree = render({
name1: ' ', value1: '100',
name2: 'Valid', value2: '25',
name3: 'Negative', value3: '-1',
name4: 'Not a number', value4: 'abc',
});

expect(rows(tree)).toHaveLength(1);
expect(textContent(rows(tree)[0])).toEqual(['Valid', '25 req/s']);
});

it('draws empty bars for all-zero values without dividing by zero', () => {
const tree = render({ value1: 0, value2: '0', value3: 0 });
const fills = rows(tree).map((row) => (row.props.children as SatoriElement[])[1].props.children[0] as SatoriElement);

expect(fills.map(({ props }) => props.style.width)).toEqual(['0%', '0%', '0%']);
expect(JSON.stringify(tree)).not.toContain('NaN');
});

it('hides the chart when every row is skipped and hides an empty note', () => {
const tree = render({
description: ' ',
name1: '', value1: 10,
name2: 'Invalid', value2: 'Infinity',
name3: 'Negative', value3: -1,
name4: 'Blank', value4: '',
});
const allText = textContent(tree);

expect(rows(tree)).toHaveLength(0);
expect(allText).not.toContain(' ');
expect(allText).not.toContain('10 req/s');
});

it('shrinks and truncates long titles, notes, and names', () => {
const tree = render({
title: 'Benchmark title '.repeat(10),
description: 'Benchmark note '.repeat(12),
name1: 'A very long benchmark name '.repeat(5),
});
const all = elements(tree);
const title = all.find((element) => element.type === 'h1');
const note = all.find((element) => element.type === 'span' && textContent(element)[0]?.startsWith('Benchmark note'));
const name = all.find((element) => element.type === 'span' && textContent(element)[0]?.startsWith('A very long benchmark'));

expect(textContent(title)[0]).toHaveLength(120);
expect(textContent(title)[0]).toMatch(/…$/);
expect(title?.props.style.fontSize).toBe('28px');
expect(textContent(note)[0]).toHaveLength(120);
expect(note?.props.style.fontSize).toBe('15px');
expect(textContent(name)[0]).toHaveLength(80);
expect(textContent(name)[0]).toMatch(/…$/);
expect(name?.props.style.fontSize).toBe('14px');
});

it('darkens the label and largest bar accent on a light background', () => {
const tree = render({ bgColor: '#ffffff', accentColor: '#fde047' });
const accentInk = mixColors('#fde047', '#000000', 0.45);
const all = elements(tree);
const label = all.find((element) => element.type === 'span' && textContent(element)[0] === 'Benchmark');
const title = all.find((element) => element.type === 'h1');
const name = rows(tree)[0].props.children[0] as SatoriElement;
const value = rows(tree)[0].props.children[2] as SatoriElement;
const largestFill = (rows(tree)[0].props.children as SatoriElement[])[1].props.children[0] as SatoriElement;

expect(label?.props.style.color).toBe(accentInk);
expect(largestFill.props.style.backgroundColor).toBe(accentInk);
expect(title?.props.style.color).toBe('#111111');
expect(name.props.style.color).toBe('#111111');
expect(value.props.style.color).toBe('#111111');
});

it('omits the unit when it is blank and trims number formatting', () => {
const tree = render({ value1: '1000', unit: ' ' });
expect(textContent(rows(tree)[0])).toContain('1,000');
expect(textContent(rows(tree)[0])).not.toContain('1,000 ');
});

it('sizes and truncates long value labels so they stay inside their column', () => {
const tree = render({ value1: '123456789.125', unit: 'requests per second '.repeat(5) });
const values = rows(tree).map((row) => row.props.children[2] as SatoriElement);

expect(textContent(values[0])[0]).toMatch(/^123,456,789\.125 requests per second/);
expect(textContent(values[0])[0]).toMatch(/…$/);
expect(textContent(values[0])[0]).toHaveLength(36);
expect(values.map(({ props }) => props.style.width)).toEqual(['360px', '360px', '360px']);
expect(values.map(({ props }) => props.style.fontSize)).toEqual(['14px', '14px', '14px']);
for (const value of values) {
expect(value.props.style.overflow).toBe('hidden');
expect(value.props.style.textOverflow).toBe('ellipsis');
expect(value.props.style.whiteSpace).toBe('nowrap');
}
});

it('uses the longest value label to size one shared column without truncating shorter rows', () => {
const tree = render({ unit: 'requests per second' });
const values = rows(tree).map((row) => row.props.children[2] as SatoriElement);

expect(values.map(({ props }) => props.style.width)).toEqual(['353px', '353px', '353px']);
expect(values.map(({ props }) => props.style.fontSize)).toEqual(['14px', '14px', '14px']);
expect(values.map((value) => textContent(value)[0])).toEqual([
'112,000 requests per second',
'71,000 requests per second',
'48,000 requests per second',
]);
});
});
Loading