9
9
* - style: labelNode.fontName.style
10
10
* - })
11
11
* - labelNode.characters = text;
12
- * + setCharacters(labelNode, text);
12
+ * + await setCharacters(labelNode, text);
13
13
* ```
14
14
*
15
15
* Provided example doesn't handle many annoying cases like, not existed or multiple fonts, which expand code a lot. `setCharacters` cover this cases and reducing noise.
16
16
*
17
- * @param node - Target node to set characters
18
- * @param characters - String of characters to set
19
- * @param fallbackFont - Font that will be applied to target node, if original will fail to load. By default is "Roboto Regular"
17
+ * @param node Target node to set characters
18
+ * @param characters String of characters to set
19
+ * @param options Parser options
20
+ * @param options.fallbackFont Font that will be applied to target node, if original will fail to load. By default is "Roboto Regular"
21
+ * @param options.smartStrategy Parser stragtegy, that allows to set font family and styles to characters in more flexible way
20
22
*/
21
23
22
- export const setCharacters = async (
24
+ import { uniqBy } from 'lodash'
25
+
26
+ interface FontLinearItem {
27
+ family : string
28
+ style : string
29
+ start ?: number
30
+ delimiter : '\n' | ' '
31
+ }
32
+
33
+ export const setCharactersCustom = async (
23
34
node : TextNode ,
24
35
characters : string ,
25
- fallbackFont ?: FontName
36
+ options ?: {
37
+ smartStrategy ?: 'prevail' | 'strict' | 'experimental'
38
+ fallbackFont ?: FontName
39
+ }
26
40
) : Promise < boolean > => {
27
- fallbackFont = fallbackFont || {
41
+ const fallbackFont = options ?. fallbackFont || {
28
42
family : 'Roboto' ,
29
43
style : 'Regular'
30
44
}
31
45
try {
32
46
if ( node . fontName === figma . mixed ) {
33
- await figma . loadFontAsync ( node . getRangeFontName ( 0 , 1 ) as FontName )
47
+ if ( options ?. smartStrategy === 'prevail' ) {
48
+ const fontHashTree : { [ key : string ] : number } = { }
49
+ for ( let i = 1 ; i < node . characters . length ; i ++ ) {
50
+ const charFont = node . getRangeFontName ( i - 1 , i ) as FontName
51
+ const key = `${ charFont . family } ::${ charFont . style } `
52
+ fontHashTree [ key ] = fontHashTree [ key ] ? fontHashTree [ key ] + 1 : 1
53
+ }
54
+ const prevailedTreeItem = Object . entries ( fontHashTree ) . sort (
55
+ ( a , b ) => b [ 1 ] - a [ 1 ]
56
+ ) [ 0 ]
57
+ const [ family , style ] = prevailedTreeItem [ 0 ] . split ( '::' )
58
+ const prevailedFont = {
59
+ family,
60
+ style
61
+ } as FontName
62
+ await figma . loadFontAsync ( prevailedFont )
63
+ node . fontName = prevailedFont
64
+ } else if ( options ?. smartStrategy === 'strict' ) {
65
+ return setCharactersWithStrictMatchFont ( node , characters , fallbackFont )
66
+ } else if ( options ?. smartStrategy === 'experimental' ) {
67
+ return setCharactersWithSmartMatchFont ( node , characters , fallbackFont )
68
+ } else {
69
+ const firstCharFont = node . getRangeFontName ( 0 , 1 ) as FontName
70
+ await figma . loadFontAsync ( firstCharFont )
71
+ node . fontName = firstCharFont
72
+ }
34
73
} else {
35
74
await figma . loadFontAsync ( {
36
75
family : node . fontName . family ,
@@ -53,3 +92,142 @@ export const setCharacters = async (
53
92
return false
54
93
}
55
94
}
95
+
96
+ const setCharactersWithStrictMatchFont = async (
97
+ node : TextNode ,
98
+ characters : string ,
99
+ fallbackFont ?: FontName
100
+ ) : Promise < boolean > => {
101
+ const fontHashTree : { [ key : string ] : string } = { }
102
+ for ( let i = 1 ; i < node . characters . length ; i ++ ) {
103
+ const startIdx = i - 1
104
+ const startCharFont = node . getRangeFontName ( startIdx , i ) as FontName
105
+ const startCharFontVal = `${ startCharFont . family } ::${ startCharFont . style } `
106
+ while ( i < node . characters . length ) {
107
+ i ++
108
+ const charFont = node . getRangeFontName ( i - 1 , i ) as FontName
109
+ if ( startCharFontVal !== `${ charFont . family } ::${ charFont . style } ` ) {
110
+ break
111
+ }
112
+ }
113
+ fontHashTree [ `${ startIdx } _${ i } ` ] = startCharFontVal
114
+ }
115
+ await figma . loadFontAsync ( fallbackFont )
116
+ node . fontName = fallbackFont
117
+ node . characters = characters
118
+ console . log ( fontHashTree )
119
+ await Promise . all (
120
+ Object . keys ( fontHashTree ) . map ( async ( range ) => {
121
+ console . log ( range , fontHashTree [ range ] )
122
+ const [ start , end ] = range . split ( '_' )
123
+ const [ family , style ] = fontHashTree [ range ] . split ( '::' )
124
+ const matchedFont = {
125
+ family,
126
+ style
127
+ } as FontName
128
+ await figma . loadFontAsync ( matchedFont )
129
+ return node . setRangeFontName ( Number ( start ) , Number ( end ) , matchedFont )
130
+ } )
131
+ )
132
+ return true
133
+ }
134
+
135
+ const getDelimiterPos = (
136
+ str : string ,
137
+ delimiter : string ,
138
+ startIdx = 0 ,
139
+ endIdx : number = str . length
140
+ ) : [ number , number ] [ ] => {
141
+ const indices = [ ]
142
+ let temp = startIdx
143
+ for ( let i = startIdx ; i < endIdx ; i ++ ) {
144
+ if ( str [ i ] === delimiter && i + startIdx !== endIdx && temp !== i + startIdx ) {
145
+ indices . push ( [ temp , i + startIdx ] )
146
+ temp = i + startIdx + 1
147
+ }
148
+ }
149
+ temp !== endIdx && indices . push ( [ temp , endIdx ] )
150
+ return indices . filter ( Boolean )
151
+ }
152
+
153
+ const buildLinearOrder = ( node : TextNode ) => {
154
+ const fontTree : FontLinearItem [ ] = [ ]
155
+ const newLinesPos = getDelimiterPos ( node . characters , '\n' )
156
+ newLinesPos . forEach ( ( [ newLinesRangeStart , newLinesRangeEnd ] , n ) => {
157
+ const newLinesRangeFont = node . getRangeFontName ( newLinesRangeStart , newLinesRangeEnd )
158
+ if ( newLinesRangeFont === figma . mixed ) {
159
+ const spacesPos = getDelimiterPos (
160
+ node . characters ,
161
+ ' ' ,
162
+ newLinesRangeStart ,
163
+ newLinesRangeEnd
164
+ )
165
+ spacesPos . forEach ( ( [ spacesRangeStart , spacesRangeEnd ] , s ) => {
166
+ const spacesRangeFont = node . getRangeFontName ( spacesRangeStart , spacesRangeEnd )
167
+ if ( spacesRangeFont === figma . mixed ) {
168
+ const spacesRangeFont = node . getRangeFontName (
169
+ spacesRangeStart ,
170
+ spacesRangeStart [ 0 ]
171
+ ) as FontName
172
+ fontTree . push ( {
173
+ start : spacesRangeStart ,
174
+ delimiter : ' ' ,
175
+ family : spacesRangeFont . family ,
176
+ style : spacesRangeFont . style
177
+ } )
178
+ } else {
179
+ fontTree . push ( {
180
+ start : spacesRangeStart ,
181
+ delimiter : ' ' ,
182
+ family : spacesRangeFont . family ,
183
+ style : spacesRangeFont . style
184
+ } )
185
+ }
186
+ } )
187
+ } else {
188
+ fontTree . push ( {
189
+ start : newLinesRangeStart ,
190
+ delimiter : '\n' ,
191
+ family : newLinesRangeFont . family ,
192
+ style : newLinesRangeFont . style
193
+ } )
194
+ }
195
+ } )
196
+ return fontTree
197
+ . sort ( ( a , b ) => + a . start - + b . start )
198
+ . map ( ( { family, style, delimiter } ) => ( { family, style, delimiter } ) )
199
+ }
200
+
201
+ const setCharactersWithSmartMatchFont = async (
202
+ node : TextNode ,
203
+ characters : string ,
204
+ fallbackFont ?: FontName
205
+ ) : Promise < boolean > => {
206
+ const rangeTree = buildLinearOrder ( node )
207
+ const fontsToLoad = uniqBy ( rangeTree , ( { family, style } ) => `${ family } ::${ style } ` ) . map (
208
+ ( { family, style } ) : FontName => ( {
209
+ family,
210
+ style
211
+ } )
212
+ )
213
+
214
+ await Promise . all ( [ ...fontsToLoad , fallbackFont ] . map ( figma . loadFontAsync ) )
215
+
216
+ node . fontName = fallbackFont
217
+ node . characters = characters
218
+
219
+ let prevPos = 0
220
+ rangeTree . forEach ( ( { family, style, delimiter } ) => {
221
+ if ( prevPos < node . characters . length ) {
222
+ const delimeterPos = node . characters . indexOf ( delimiter , prevPos )
223
+ const endPos = delimeterPos > prevPos ? delimeterPos : node . characters . length
224
+ const matchedFont = {
225
+ family,
226
+ style
227
+ }
228
+ node . setRangeFontName ( prevPos , endPos , matchedFont )
229
+ prevPos = endPos + 1
230
+ }
231
+ } )
232
+ return true
233
+ }
0 commit comments