11import { buildXMLString } from '../utils.js'
2+ import type { GlyphBox } from '../font.js'
3+
4+ function buildSkipInkSegments (
5+ start : number ,
6+ end : number ,
7+ glyphBoxes : GlyphBox [ ] ,
8+ y : number ,
9+ strokeWidth : number ,
10+ baseline : number
11+ ) {
12+ const halfStroke = strokeWidth / 2
13+ const bleed = Math . max ( halfStroke , strokeWidth * 1.25 )
14+ const skipRanges : [ number , number ] [ ] = [ ]
15+
16+ for ( const box of glyphBoxes ) {
17+ // Only skip glyphs that actually cross the underline position and extend below the baseline.
18+ if ( box . y2 < baseline + halfStroke || box . y1 > y + halfStroke ) continue
19+
20+ const from = Math . max ( start , box . x1 - bleed )
21+ const to = Math . min ( end , box . x2 + bleed )
22+
23+ if ( from >= to ) continue
24+ if ( skipRanges . length === 0 ) {
25+ skipRanges . push ( [ from , to ] )
26+ continue
27+ }
28+
29+ const last = skipRanges [ skipRanges . length - 1 ]
30+ if ( from <= last [ 1 ] ) {
31+ last [ 1 ] = Math . max ( last [ 1 ] , to )
32+ } else {
33+ skipRanges . push ( [ from , to ] )
34+ }
35+ }
36+
37+ if ( ! skipRanges . length ) {
38+ return [ [ start , end ] ] as [ number , number ] [ ]
39+ }
40+
41+ const segments : [ number , number ] [ ] = [ ]
42+ let cursor = start
43+
44+ for ( const [ from , to ] of skipRanges ) {
45+ if ( from > cursor ) {
46+ segments . push ( [ cursor , from ] )
47+ }
48+ cursor = Math . max ( cursor , to )
49+ if ( cursor >= end ) break
50+ }
51+
52+ if ( cursor < end ) {
53+ segments . push ( [ cursor , end ] )
54+ }
55+
56+ return segments
57+ }
258
359export default function buildDecoration (
460 {
@@ -8,20 +64,23 @@ export default function buildDecoration(
864 ascender,
965 clipPathId,
1066 matrix,
67+ glyphBoxes,
1168 } : {
1269 width : number
1370 left : number
1471 top : number
1572 ascender : number
1673 clipPathId ?: string
1774 matrix ?: string
75+ glyphBoxes ?: GlyphBox [ ]
1876 } ,
1977 style : Record < string , any >
2078) {
2179 const {
2280 textDecorationColor,
2381 textDecorationStyle,
2482 textDecorationLine,
83+ textDecorationSkipInk,
2584 fontSize,
2685 color,
2786 } = style
@@ -45,36 +104,56 @@ export default function buildDecoration(
45104 ? `0 ${ height * 2 } `
46105 : undefined
47106
107+ const applySkipInk =
108+ textDecorationLine === 'underline' &&
109+ ( textDecorationSkipInk || 'auto' ) !== 'none' &&
110+ glyphBoxes ?. length
111+
112+ const baseline = top + ascender
113+
114+ const segments = applySkipInk
115+ ? buildSkipInkSegments ( left , left + width , glyphBoxes , y , height , baseline )
116+ : ( [ [ left , left + width ] ] as [ number , number ] [ ] )
117+
48118 // https://www.w3.org/TR/css-backgrounds-3/#valdef-line-style-double
49119 const extraLine =
50120 textDecorationStyle === 'double'
51- ? buildXMLString ( 'line' , {
52- x1 : left ,
53- y1 : y + height + 1 ,
54- x2 : left + width ,
55- y2 : y + height + 1 ,
121+ ? segments
122+ . map ( ( [ x1 , x2 ] ) =>
123+ buildXMLString ( 'line' , {
124+ x1,
125+ y1 : y + height + 1 ,
126+ x2,
127+ y2 : y + height + 1 ,
128+ stroke : textDecorationColor || color ,
129+ 'stroke-width' : height ,
130+ 'stroke-dasharray' : dasharray ,
131+ 'stroke-linecap' :
132+ textDecorationStyle === 'dotted' ? 'round' : 'square' ,
133+ transform : matrix ,
134+ } )
135+ )
136+ . join ( '' )
137+ : ''
138+
139+ return (
140+ ( clipPathId ? `<g clip-path="url(#${ clipPathId } )">` : '' ) +
141+ segments
142+ . map ( ( [ x1 , x2 ] ) =>
143+ buildXMLString ( 'line' , {
144+ x1,
145+ y1 : y ,
146+ x2,
147+ y2 : y ,
56148 stroke : textDecorationColor || color ,
57149 'stroke-width' : height ,
58150 'stroke-dasharray' : dasharray ,
59151 'stroke-linecap' :
60152 textDecorationStyle === 'dotted' ? 'round' : 'square' ,
61153 transform : matrix ,
62154 } )
63- : ''
64-
65- return (
66- ( clipPathId ? `<g clip-path="url(#${ clipPathId } )">` : '' ) +
67- buildXMLString ( 'line' , {
68- x1 : left ,
69- y1 : y ,
70- x2 : left + width ,
71- y2 : y ,
72- stroke : textDecorationColor || color ,
73- 'stroke-width' : height ,
74- 'stroke-dasharray' : dasharray ,
75- 'stroke-linecap' : textDecorationStyle === 'dotted' ? 'round' : 'square' ,
76- transform : matrix ,
77- } ) +
155+ )
156+ . join ( '' ) +
78157 extraLine +
79158 ( clipPathId ? '</g>' : '' )
80159 )
0 commit comments