@@ -36,11 +36,14 @@ const COLORS = {
3636}
3737
3838const CHAR_WIDTH = 7
39+ const SHIELDS_CHAR_WIDTH = 6
3940
4041const BADGE_PADDING_X = 8
4142const MIN_BADGE_TEXT_WIDTH = 40
43+ const SHIELDS_LABEL_PADDING_X = 5
4244
4345const BADGE_FONT_SHORTHAND = 'normal normal 400 11px Geist, system-ui, -apple-system, sans-serif'
46+ const SHIELDS_FONT_SHORTHAND = 'normal normal 400 11px Verdana, Geneva, DejaVu Sans, sans-serif'
4447
4548let cachedCanvasContext : SKRSContext2D | null | undefined
4649
@@ -58,24 +61,119 @@ function getCanvasContext(): SKRSContext2D | null {
5861 return cachedCanvasContext
5962}
6063
61- function fallbackMeasureTextWidth ( text : string ) : number {
64+ function OLD_measureTextWidth ( text : string ) : number {
6265 return Math . max ( MIN_BADGE_TEXT_WIDTH , Math . round ( text . length * CHAR_WIDTH ) + BADGE_PADDING_X * 2 )
6366}
6467
65- function measureTextWidth ( text : string ) : number {
68+ function measureText ( text : string , font : string ) : number | null {
6669 const context = getCanvasContext ( )
6770
6871 if ( context ) {
69- context . font = BADGE_FONT_SHORTHAND
72+ context . font = font
7073
7174 const measuredWidth = context . measureText ( text ) . width
7275
73- if ( ! Number . isNaN ( measuredWidth ) ) {
74- return Math . max ( MIN_BADGE_TEXT_WIDTH , Math . ceil ( measuredWidth ) + BADGE_PADDING_X * 2 )
76+ if ( Number . isFinite ( measuredWidth ) ) {
77+ return Math . ceil ( measuredWidth )
7578 }
7679 }
7780
78- return fallbackMeasureTextWidth ( text )
81+ return null
82+ }
83+
84+ function measureDefaultTextWidth ( text : string ) : number {
85+ const measuredWidth = measureText ( text , BADGE_FONT_SHORTHAND )
86+
87+ if ( measuredWidth !== null ) {
88+ return Math . max ( MIN_BADGE_TEXT_WIDTH , measuredWidth + BADGE_PADDING_X * 2 )
89+ }
90+
91+ return OLD_measureTextWidth ( text )
92+ }
93+
94+ function measureShieldsTextLength ( text : string ) : number {
95+ const measuredWidth = measureText ( text , SHIELDS_FONT_SHORTHAND )
96+
97+ if ( measuredWidth !== null ) {
98+ return Math . max ( 1 , measuredWidth )
99+ }
100+
101+ return Math . max ( 1 , Math . round ( text . length * SHIELDS_CHAR_WIDTH ) )
102+ }
103+
104+ function renderDefaultBadgeSvg ( params : {
105+ finalColor : string
106+ finalLabel : string
107+ finalLabelColor : string
108+ finalValue : string
109+ } ) : string {
110+ const { finalColor, finalLabel, finalLabelColor, finalValue } = params
111+ const leftWidth = finalLabel . trim ( ) . length === 0 ? 0 : measureDefaultTextWidth ( finalLabel )
112+ const rightWidth = measureDefaultTextWidth ( finalValue )
113+ const totalWidth = leftWidth + rightWidth
114+ const height = 20
115+
116+ return `
117+ <svg xmlns="http://www.w3.org/2000/svg" width="${ totalWidth } " height="${ height } " role="img" aria-label="${ finalLabel } : ${ finalValue } ">
118+ <clipPath id="r">
119+ <rect width="${ totalWidth } " height="${ height } " rx="3" fill="#fff"/>
120+ </clipPath>
121+ <g clip-path="url(#r)">
122+ <rect width="${ leftWidth } " height="${ height } " fill="${ finalLabelColor } "/>
123+ <rect x="${ leftWidth } " width="${ rightWidth } " height="${ height } " fill="${ finalColor } "/>
124+ </g>
125+ <g text-anchor="middle" font-family="Geist, system-ui, -apple-system, sans-serif" font-size="11">
126+ <text x="${ leftWidth / 2 } " y="14" fill="#ffffff">${ finalLabel } </text>
127+ <text x="${ leftWidth + rightWidth / 2 } " y="14" fill="#ffffff">${ finalValue } </text>
128+ </g>
129+ </svg>
130+ ` . trim ( )
131+ }
132+
133+ function renderShieldsBadgeSvg ( params : {
134+ finalColor : string
135+ finalLabel : string
136+ finalLabelColor : string
137+ finalValue : string
138+ } ) : string {
139+ const { finalColor, finalLabel, finalLabelColor, finalValue } = params
140+ const hasLabel = finalLabel . trim ( ) . length > 0
141+
142+ const leftTextLength = hasLabel ? measureShieldsTextLength ( finalLabel ) : 0
143+ const rightTextLength = measureShieldsTextLength ( finalValue )
144+ const leftWidth = hasLabel ? leftTextLength + SHIELDS_LABEL_PADDING_X * 2 : 0
145+ const rightWidth = rightTextLength + SHIELDS_LABEL_PADDING_X * 2
146+ const totalWidth = leftWidth + rightWidth
147+ const height = 20
148+ const title = `${ finalLabel } : ${ finalValue } `
149+
150+ const leftCenter = Math . round ( ( leftWidth / 2 ) * 10 )
151+ const rightCenter = Math . round ( ( leftWidth + rightWidth / 2 ) * 10 )
152+ const leftTextLengthAttr = leftTextLength * 10
153+ const rightTextLengthAttr = rightTextLength * 10
154+
155+ return `
156+ <svg xmlns="http://www.w3.org/2000/svg" width="${ totalWidth } " height="${ height } " role="img" aria-label="${ title } ">
157+ <linearGradient id="s" x2="0" y2="100%">
158+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
159+ <stop offset="1" stop-opacity=".1"/>
160+ </linearGradient>
161+ <clipPath id="r">
162+ <rect width="${ totalWidth } " height="${ height } " rx="3" fill="#fff"/>
163+ </clipPath>
164+ <g clip-path="url(#r)">
165+ <rect width="${ leftWidth } " height="${ height } " fill="${ finalLabelColor } "/>
166+ <rect x="${ leftWidth } " width="${ rightWidth } " height="${ height } " fill="${ finalColor } "/>
167+ <rect width="${ totalWidth } " height="${ height } " fill="url(#s)"/>
168+ </g>
169+ <g fill="#fff" text-anchor="middle" font-family="Verdana, Geneva, DejaVu Sans, sans-serif" text-rendering="geometricPrecision" font-size="110">
170+ <text aria-hidden="true" x="${ leftCenter } " y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${ leftTextLengthAttr } ">${ finalLabel } </text>
171+ <text x="${ leftCenter } " y="140" transform="scale(.1)" fill="#fff" textLength="${ leftTextLengthAttr } ">${ finalLabel } </text>
172+ <text aria-hidden="true" x="${ rightCenter } " y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${ rightTextLengthAttr } ">${ finalValue } </text>
173+ <text x="${ rightCenter } " y="140" transform="scale(.1)" fill="#fff" textLength="${ rightTextLengthAttr } ">${ finalValue } </text>
174+ </g>
175+ </svg>
176+ ` . trim ( )
79177}
80178
81179function formatBytes ( bytes : number ) : string {
@@ -284,6 +382,7 @@ const badgeStrategies = {
284382}
285383
286384const BadgeTypeSchema = v . picklist ( Object . keys ( badgeStrategies ) as [ string , ...string [ ] ] )
385+ const BadgeStyleSchema = v . picklist ( [ 'default' , 'shieldsio' ] )
287386
288387export default defineCachedEventHandler (
289388 async event => {
@@ -309,6 +408,8 @@ export default defineCachedEventHandler(
309408 const labelColor = queryParams . success ? queryParams . output . labelColor : undefined
310409 const showName = queryParams . success && queryParams . output . name === 'true'
311410 const userLabel = queryParams . success ? queryParams . output . label : undefined
411+ const badgeStyleResult = v . safeParse ( BadgeStyleSchema , query . style )
412+ const badgeStyle = badgeStyleResult . success ? badgeStyleResult . output : 'default'
312413
313414 const badgeTypeResult = v . safeParse ( BadgeTypeSchema , typeParam )
314415 const strategyKey = badgeTypeResult . success ? badgeTypeResult . output : 'version'
@@ -325,29 +426,12 @@ export default defineCachedEventHandler(
325426 const rawColor = userColor ?? strategyResult . color
326427 const finalColor = rawColor ?. startsWith ( '#' ) ? rawColor : `#${ rawColor } `
327428
328- const rawLabelColor = labelColor ?? '#0a0a0a'
329- const finalLabelColor = rawLabelColor ?. startsWith ( '#' ) ? rawLabelColor : `#${ rawLabelColor } `
330-
331- const leftWidth = finalLabel . trim ( ) . length === 0 ? 0 : measureTextWidth ( finalLabel )
332- const rightWidth = measureTextWidth ( finalValue )
333- const totalWidth = leftWidth + rightWidth
334- const height = 20
335-
336- const svg = `
337- <svg xmlns="http://www.w3.org/2000/svg" width="${ totalWidth } " height="${ height } " role="img" aria-label="${ finalLabel } : ${ finalValue } ">
338- <clipPath id="r">
339- <rect width="${ totalWidth } " height="${ height } " rx="3" fill="#fff"/>
340- </clipPath>
341- <g clip-path="url(#r)">
342- <rect width="${ leftWidth } " height="${ height } " fill="${ finalLabelColor } "/>
343- <rect x="${ leftWidth } " width="${ rightWidth } " height="${ height } " fill="${ finalColor } "/>
344- </g>
345- <g text-anchor="middle" font-family="'Geist', system-ui, -apple-system, sans-serif" font-size="11">
346- <text x="${ leftWidth / 2 } " y="14" fill="#ffffff">${ finalLabel } </text>
347- <text x="${ leftWidth + rightWidth / 2 } " y="14" fill="#ffffff">${ finalValue } </text>
348- </g>
349- </svg>
350- ` . trim ( )
429+ const defaultLabelColor = badgeStyle === 'shieldsio' ? '#555' : '#0a0a0a'
430+ const rawLabelColor = labelColor ?? defaultLabelColor
431+ const finalLabelColor = rawLabelColor . startsWith ( '#' ) ? rawLabelColor : `#${ rawLabelColor } `
432+
433+ const renderFn = badgeStyle === 'shieldsio' ? renderShieldsBadgeSvg : renderDefaultBadgeSvg
434+ const svg = renderFn ( { finalColor, finalLabel, finalLabelColor, finalValue } )
351435
352436 setHeader ( event , 'Content-Type' , 'image/svg+xml' )
353437 setHeader (
0 commit comments