Skip to content

Commit a19b467

Browse files
feat: Added strategies to setCharacters helper
1 parent fa316f0 commit a19b467

File tree

1 file changed

+186
-8
lines changed

1 file changed

+186
-8
lines changed

src/helpers/setCharacters.ts

+186-8
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,67 @@
99
* - style: labelNode.fontName.style
1010
* - })
1111
* - labelNode.characters = text;
12-
* + setCharacters(labelNode, text);
12+
* + await setCharacters(labelNode, text);
1313
* ```
1414
*
1515
* 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.
1616
*
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
2022
*/
2123

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 (
2334
node: TextNode,
2435
characters: string,
25-
fallbackFont?: FontName
36+
options?: {
37+
smartStrategy?: 'prevail' | 'strict' | 'experimental'
38+
fallbackFont?: FontName
39+
}
2640
): Promise<boolean> => {
27-
fallbackFont = fallbackFont || {
41+
const fallbackFont = options?.fallbackFont || {
2842
family: 'Roboto',
2943
style: 'Regular'
3044
}
3145
try {
3246
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+
}
3473
} else {
3574
await figma.loadFontAsync({
3675
family: node.fontName.family,
@@ -53,3 +92,142 @@ export const setCharacters = async (
5392
return false
5493
}
5594
}
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

Comments
 (0)