diff --git a/server/api/registry/badge/[type]/[...pkg].get.ts b/server/api/registry/badge/[type]/[...pkg].get.ts index 2ce404e381..194e89b3f6 100644 --- a/server/api/registry/badge/[type]/[...pkg].get.ts +++ b/server/api/registry/badge/[type]/[...pkg].get.ts @@ -41,11 +41,9 @@ const COLORS = { white: '#ffffff', } -const CHAR_WIDTH = 7 -const SHIELDS_CHAR_WIDTH = 6 - const BADGE_PADDING_X = 8 const MIN_BADGE_TEXT_WIDTH = 40 +const FALLBACK_VALUE_EXTRA_PADDING_X = 8 const SHIELDS_LABEL_PADDING_X = 5 const BADGE_FONT_SHORTHAND = 'normal normal 400 11px Geist, system-ui, -apple-system, sans-serif' @@ -53,6 +51,77 @@ const SHIELDS_FONT_SHORTHAND = 'normal normal 400 11px Verdana, Geneva, DejaVu S let cachedCanvasContext: SKRSContext2D | null | undefined +const NARROW_CHARS = new Set([' ', '!', '"', "'", '(', ')', '*', ',', '-', '.', ':', ';', '|']) +const MEDIUM_CHARS = new Set([ + '#', + '$', + '+', + '/', + '<', + '=', + '>', + '?', + '@', + '[', + '\\', + ']', + '^', + '_', + '`', + '{', + '}', + '~', +]) + +const FALLBACK_WIDTHS = { + default: { + narrow: 3, + medium: 5, + digit: 6, + uppercase: 7, + other: 6, + }, + shieldsio: { + narrow: 3, + medium: 5, + digit: 6, + uppercase: 7, + other: 5.5, + }, +} as const + +function estimateTextWidth(text: string, fallbackFont: 'default' | 'shieldsio'): number { + // Heuristic coefficients tuned to keep fallback rendering close to canvas metrics. + const widths = FALLBACK_WIDTHS[fallbackFont] + let totalWidth = 0 + + for (const character of text) { + if (NARROW_CHARS.has(character)) { + totalWidth += widths.narrow + continue + } + + if (MEDIUM_CHARS.has(character)) { + totalWidth += widths.medium + continue + } + + if (/\d/.test(character)) { + totalWidth += widths.digit + continue + } + + if (/[A-Z]/.test(character)) { + totalWidth += widths.uppercase + continue + } + + totalWidth += widths.other + } + + return Math.max(1, Math.round(totalWidth)) +} + function getCanvasContext(): SKRSContext2D | null { if (cachedCanvasContext !== undefined) { return cachedCanvasContext @@ -83,14 +152,17 @@ function measureTextWidth(text: string, font: string): number | null { return null } -function measureDefaultTextWidth(text: string): number { +function measureDefaultTextWidth(text: string, fallbackExtraPadding = 0): number { const measuredWidth = measureTextWidth(text, BADGE_FONT_SHORTHAND) if (measuredWidth !== null) { return Math.max(MIN_BADGE_TEXT_WIDTH, measuredWidth + BADGE_PADDING_X * 2) } - return Math.max(MIN_BADGE_TEXT_WIDTH, Math.round(text.length * CHAR_WIDTH) + BADGE_PADDING_X * 2) + return Math.max( + MIN_BADGE_TEXT_WIDTH, + estimateTextWidth(text, 'default') + BADGE_PADDING_X * 2 + fallbackExtraPadding, + ) } function escapeXML(str: string): string { @@ -125,7 +197,7 @@ function measureShieldsTextLength(text: string): number { return Math.max(1, measuredWidth) } - return Math.max(1, Math.round(text.length * SHIELDS_CHAR_WIDTH)) + return estimateTextWidth(text, 'shieldsio') } function renderDefaultBadgeSvg(params: { @@ -139,7 +211,7 @@ function renderDefaultBadgeSvg(params: { const { finalColor, finalLabel, finalLabelColor, finalValue, labelTextColor, valueTextColor } = params const leftWidth = finalLabel.trim().length === 0 ? 0 : measureDefaultTextWidth(finalLabel) - const rightWidth = measureDefaultTextWidth(finalValue) + const rightWidth = measureDefaultTextWidth(finalValue, FALLBACK_VALUE_EXTRA_PADDING_X) const totalWidth = leftWidth + rightWidth const height = 20 const escapedLabel = escapeXML(finalLabel)