@@ -18,7 +18,39 @@ function escapeXml(value: string) {
1818 . replace ( / ' / g, ''' )
1919}
2020
21- function wrapText ( value : string , maxChars : number , maxLines : number ) {
21+ function glyphWidthFactor ( char : string ) {
22+ if ( char === ' ' ) return 0.28
23+ if ( char === '…' ) return 0.62
24+ if ( / [ i l I . , : ; | ! ' " ` ] / . test ( char ) ) return 0.28
25+ if ( / [ m w M W @ % & ] / . test ( char ) ) return 0.9
26+ if ( / [ A - Z ] / . test ( char ) ) return 0.68
27+ if ( / [ 0 - 9 ] / . test ( char ) ) return 0.6
28+ return 0.56
29+ }
30+
31+ function estimateTextWidth ( value : string , fontSize : number ) {
32+ let width = 0
33+ for ( const char of value ) width += glyphWidthFactor ( char ) * fontSize
34+ return width
35+ }
36+
37+ function truncateToWidth ( value : string , maxWidth : number , fontSize : number ) {
38+ const trimmed = value . trim ( )
39+ if ( ! trimmed ) return ''
40+ if ( estimateTextWidth ( trimmed , fontSize ) <= maxWidth ) return trimmed
41+
42+ const ellipsis = '…'
43+ const ellipsisWidth = estimateTextWidth ( ellipsis , fontSize )
44+ let out = ''
45+ for ( const char of trimmed ) {
46+ const next = out + char
47+ if ( estimateTextWidth ( next , fontSize ) + ellipsisWidth > maxWidth ) break
48+ out = next
49+ }
50+ return `${ out . replace ( / \s + $ / g, '' ) . replace ( / [ . 。 , ; : ! ? ] + $ / g, '' ) } ${ ellipsis } `
51+ }
52+
53+ function wrapText ( value : string , maxWidth : number , fontSize : number , maxLines : number ) {
2254 const words = value . trim ( ) . split ( / \s + / ) . filter ( Boolean )
2355 const lines : string [ ] = [ ]
2456 let current = ''
@@ -29,19 +61,26 @@ function wrapText(value: string, maxChars: number, maxLines: number) {
2961 }
3062
3163 function splitLongWord ( word : string ) {
32- if ( word . length <= maxChars ) return [ word ]
64+ if ( estimateTextWidth ( word , fontSize ) <= maxWidth ) return [ word ]
3365 const parts : string [ ] = [ ]
3466 let remaining = word
35- while ( remaining . length > maxChars ) {
36- parts . push ( `${ remaining . slice ( 0 , maxChars - 1 ) } …` )
37- remaining = remaining . slice ( maxChars - 1 )
67+ while ( remaining && estimateTextWidth ( remaining , fontSize ) > maxWidth ) {
68+ let chunk = ''
69+ for ( const char of remaining ) {
70+ const next = chunk + char
71+ if ( estimateTextWidth ( `${ next } …` , fontSize ) > maxWidth ) break
72+ chunk = next
73+ }
74+ if ( ! chunk ) break
75+ parts . push ( `${ chunk } …` )
76+ remaining = remaining . slice ( chunk . length )
3877 }
3978 if ( remaining ) parts . push ( remaining )
4079 return parts
4180 }
4281
4382 for ( const word of words ) {
44- if ( word . length > maxChars ) {
83+ if ( estimateTextWidth ( word , fontSize ) > maxWidth ) {
4584 if ( current ) {
4685 pushLine ( current )
4786 current = ''
@@ -58,7 +97,7 @@ function wrapText(value: string, maxChars: number, maxLines: number) {
5897 }
5998
6099 const next = current ? `${ current } ${ word } ` : word
61- if ( next . length <= maxChars ) {
100+ if ( estimateTextWidth ( next , fontSize ) <= maxWidth ) {
62101 current = next
63102 continue
64103 }
@@ -71,9 +110,7 @@ function wrapText(value: string, maxChars: number, maxLines: number) {
71110
72111 const usedWords = lines . join ( ' ' ) . split ( / \s + / ) . filter ( Boolean ) . length
73112 if ( usedWords < words . length ) {
74- const last = lines . at ( - 1 ) ?? ''
75- const trimmed = last . length > maxChars ? last . slice ( 0 , maxChars ) : last
76- lines [ lines . length - 1 ] = `${ trimmed . replace ( / \s + $ / g, '' ) . replace ( / [ . 。 , ; : ! ? ] + $ / g, '' ) } …`
113+ lines [ lines . length - 1 ] = truncateToWidth ( lines . at ( - 1 ) ?? '' , maxWidth , fontSize )
77114 }
78115 return lines
79116}
@@ -88,10 +125,18 @@ export function buildSkillOgSvg(params: SkillOgSvgParams) {
88125 const cardH = 456
89126 const cardR = 34
90127
91- const titleLines = wrapText ( rawTitle , 22 , 2 )
92- const descLines = wrapText ( rawDescription , 42 , 3 )
128+ const contentX = 114
129+ const contentRightPadding = 28
130+ const contentMaxWidth = cardX + cardW - contentX - contentRightPadding
131+
132+ const titleMaxLines = 2
133+ const descMaxLines = 3
134+
135+ const titleProbeLines = wrapText ( rawTitle , contentMaxWidth , 80 , titleMaxLines )
136+ const titleFontSize = titleProbeLines . length > 1 ? 72 : 80
137+ const titleLines = wrapText ( rawTitle , contentMaxWidth , titleFontSize , titleMaxLines )
93138
94- const titleFontSize = titleLines . length > 1 || rawTitle . length > 24 ? 72 : 80
139+ const descLines = wrapText ( rawDescription , contentMaxWidth , 26 , descMaxLines )
95140 const titleY = titleLines . length > 1 ? 258 : 280
96141 const titleLineHeight = 84
97142
0 commit comments