@@ -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,115 @@ function getCanvasContext(): SKRSContext2D | null {
5861 return cachedCanvasContext
5962}
6063
61- function fallbackMeasureTextWidth ( text : string ) : number {
62- return Math . max ( MIN_BADGE_TEXT_WIDTH , Math . round ( text . length * CHAR_WIDTH ) + BADGE_PADDING_X * 2 )
63- }
64-
65- function measureTextWidth ( text : string ) : number {
64+ function measureTextWidth ( text : string , font : string ) : number | null {
6665 const context = getCanvasContext ( )
6766
6867 if ( context ) {
69- context . font = BADGE_FONT_SHORTHAND
68+ context . font = font
7069
7170 const measuredWidth = context . measureText ( text ) . width
7271
7372 if ( ! Number . isNaN ( measuredWidth ) ) {
74- return Math . max ( MIN_BADGE_TEXT_WIDTH , Math . ceil ( measuredWidth ) + BADGE_PADDING_X * 2 )
73+ return Math . ceil ( measuredWidth )
7574 }
7675 }
7776
78- return fallbackMeasureTextWidth ( text )
77+ return null
78+ }
79+
80+ function measureDefaultTextWidth ( text : string ) : number {
81+ const measuredWidth = measureTextWidth ( text , BADGE_FONT_SHORTHAND )
82+
83+ if ( measuredWidth !== null ) {
84+ return Math . max ( MIN_BADGE_TEXT_WIDTH , measuredWidth + BADGE_PADDING_X * 2 )
85+ }
86+
87+ return Math . max ( MIN_BADGE_TEXT_WIDTH , Math . round ( text . length * CHAR_WIDTH ) + BADGE_PADDING_X * 2 )
88+ }
89+
90+ function measureShieldsTextLength ( text : string ) : number {
91+ const measuredWidth = measureTextWidth ( text , SHIELDS_FONT_SHORTHAND )
92+
93+ if ( measuredWidth !== null ) {
94+ return Math . max ( 1 , measuredWidth )
95+ }
96+
97+ return Math . max ( 1 , Math . round ( text . length * SHIELDS_CHAR_WIDTH ) )
98+ }
99+
100+ function renderDefaultBadgeSvg ( params : {
101+ finalColor : string
102+ finalLabel : string
103+ finalLabelColor : string
104+ finalValue : string
105+ } ) : string {
106+ const { finalColor, finalLabel, finalLabelColor, finalValue } = params
107+ const leftWidth = finalLabel . trim ( ) . length === 0 ? 0 : measureDefaultTextWidth ( finalLabel )
108+ const rightWidth = measureDefaultTextWidth ( finalValue )
109+ const totalWidth = leftWidth + rightWidth
110+ const height = 20
111+
112+ return `
113+ <svg xmlns="http://www.w3.org/2000/svg" width="${ totalWidth } " height="${ height } " role="img" aria-label="${ finalLabel } : ${ finalValue } ">
114+ <clipPath id="r">
115+ <rect width="${ totalWidth } " height="${ height } " rx="3" fill="#fff"/>
116+ </clipPath>
117+ <g clip-path="url(#r)">
118+ <rect width="${ leftWidth } " height="${ height } " fill="${ finalLabelColor } "/>
119+ <rect x="${ leftWidth } " width="${ rightWidth } " height="${ height } " fill="${ finalColor } "/>
120+ </g>
121+ <g text-anchor="middle" font-family="Geist, system-ui, -apple-system, sans-serif" font-size="11">
122+ <text x="${ leftWidth / 2 } " y="14" fill="#ffffff">${ finalLabel } </text>
123+ <text x="${ leftWidth + rightWidth / 2 } " y="14" fill="#ffffff">${ finalValue } </text>
124+ </g>
125+ </svg>
126+ ` . trim ( )
127+ }
128+
129+ function renderShieldsBadgeSvg ( params : {
130+ finalColor : string
131+ finalLabel : string
132+ finalLabelColor : string
133+ finalValue : string
134+ } ) : string {
135+ const { finalColor, finalLabel, finalLabelColor, finalValue } = params
136+ const hasLabel = finalLabel . trim ( ) . length > 0
137+
138+ const leftTextLength = hasLabel ? measureShieldsTextLength ( finalLabel ) : 0
139+ const rightTextLength = measureShieldsTextLength ( finalValue )
140+ const leftWidth = hasLabel ? leftTextLength + SHIELDS_LABEL_PADDING_X * 2 : 0
141+ const rightWidth = rightTextLength + SHIELDS_LABEL_PADDING_X * 2
142+ const totalWidth = leftWidth + rightWidth
143+ const height = 20
144+ const title = `${ finalLabel } : ${ finalValue } `
145+
146+ const leftCenter = Math . round ( ( leftWidth / 2 ) * 10 )
147+ const rightCenter = Math . round ( ( leftWidth + rightWidth / 2 ) * 10 )
148+ const leftTextLengthAttr = leftTextLength * 10
149+ const rightTextLengthAttr = rightTextLength * 10
150+
151+ return `
152+ <svg xmlns="http://www.w3.org/2000/svg" width="${ totalWidth } " height="${ height } " role="img" aria-label="${ title } ">
153+ <linearGradient id="s" x2="0" y2="100%">
154+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
155+ <stop offset="1" stop-opacity=".1"/>
156+ </linearGradient>
157+ <clipPath id="r">
158+ <rect width="${ totalWidth } " height="${ height } " rx="3" fill="#fff"/>
159+ </clipPath>
160+ <g clip-path="url(#r)">
161+ <rect width="${ leftWidth } " height="${ height } " fill="${ finalLabelColor } "/>
162+ <rect x="${ leftWidth } " width="${ rightWidth } " height="${ height } " fill="${ finalColor } "/>
163+ <rect width="${ totalWidth } " height="${ height } " fill="url(#s)"/>
164+ </g>
165+ <g fill="#fff" text-anchor="middle" font-family="Verdana, Geneva, DejaVu Sans, sans-serif" text-rendering="geometricPrecision" font-size="110">
166+ <text aria-hidden="true" x="${ leftCenter } " y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${ leftTextLengthAttr } ">${ finalLabel } </text>
167+ <text x="${ leftCenter } " y="140" transform="scale(.1)" fill="#fff" textLength="${ leftTextLengthAttr } ">${ finalLabel } </text>
168+ <text aria-hidden="true" x="${ rightCenter } " y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${ rightTextLengthAttr } ">${ finalValue } </text>
169+ <text x="${ rightCenter } " y="140" transform="scale(.1)" fill="#fff" textLength="${ rightTextLengthAttr } ">${ finalValue } </text>
170+ </g>
171+ </svg>
172+ ` . trim ( )
79173}
80174
81175function formatBytes ( bytes : number ) : string {
@@ -288,6 +382,7 @@ const badgeStrategies = {
288382}
289383
290384const BadgeTypeSchema = v . picklist ( Object . keys ( badgeStrategies ) as [ string , ...string [ ] ] )
385+ const BadgeStyleSchema = v . picklist ( [ 'default' , 'shieldsio' ] )
291386
292387export default defineCachedEventHandler (
293388 async event => {
@@ -313,6 +408,8 @@ export default defineCachedEventHandler(
313408 const labelColor = queryParams . success ? queryParams . output . labelColor : undefined
314409 const showName = queryParams . success && queryParams . output . name === 'true'
315410 const userLabel = queryParams . success ? queryParams . output . label : undefined
411+ const badgeStyleResult = v . safeParse ( BadgeStyleSchema , query . style )
412+ const badgeStyle = badgeStyleResult . success ? badgeStyleResult . output : 'default'
316413
317414 const badgeTypeResult = v . safeParse ( BadgeTypeSchema , typeParam )
318415 const strategyKey = badgeTypeResult . success ? badgeTypeResult . output : 'version'
@@ -329,29 +426,12 @@ export default defineCachedEventHandler(
329426 const rawColor = userColor ?? strategyResult . color
330427 const finalColor = rawColor ?. startsWith ( '#' ) ? rawColor : `#${ rawColor } `
331428
332- const rawLabelColor = labelColor ?? '#0a0a0a'
333- const finalLabelColor = rawLabelColor ?. startsWith ( '#' ) ? rawLabelColor : `#${ rawLabelColor } `
334-
335- const leftWidth = finalLabel . trim ( ) . length === 0 ? 0 : measureTextWidth ( finalLabel )
336- const rightWidth = measureTextWidth ( finalValue )
337- const totalWidth = leftWidth + rightWidth
338- const height = 20
339-
340- const svg = `
341- <svg xmlns="http://www.w3.org/2000/svg" width="${ totalWidth } " height="${ height } " role="img" aria-label="${ finalLabel } : ${ finalValue } ">
342- <clipPath id="r">
343- <rect width="${ totalWidth } " height="${ height } " rx="3" fill="#fff"/>
344- </clipPath>
345- <g clip-path="url(#r)">
346- <rect width="${ leftWidth } " height="${ height } " fill="${ finalLabelColor } "/>
347- <rect x="${ leftWidth } " width="${ rightWidth } " height="${ height } " fill="${ finalColor } "/>
348- </g>
349- <g text-anchor="middle" font-family="'Geist', system-ui, -apple-system, sans-serif" font-size="11">
350- <text x="${ leftWidth / 2 } " y="14" fill="#ffffff">${ finalLabel } </text>
351- <text x="${ leftWidth + rightWidth / 2 } " y="14" fill="#ffffff">${ finalValue } </text>
352- </g>
353- </svg>
354- ` . 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 } )
355435
356436 setHeader ( event , 'Content-Type' , 'image/svg+xml' )
357437 setHeader (
0 commit comments