-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathparseTextStyle.ts
More file actions
418 lines (354 loc) · 9.13 KB
/
parseTextStyle.ts
File metadata and controls
418 lines (354 loc) · 9.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
import { isEqual, cloneDeep, uniqWith } from 'lodash'
import getAllFonts from './getAllFonts'
import loadFonts from './loadFonts'
const styleFonts: FontStyleNames[] = [
'fontSize',
'fontName',
'textCase',
'textDecoration',
'letterSpacing',
'lineHeight',
'fills',
'textStyleId',
'fillStyleId',
'hyperlink'
]
/*
The function returns the text node styles, splitting them into different arrays, such as:
[{
characters: "...",
... (styles)
}, ...]
---
Returns styles for the entire text:
parseTextStyle(textNode)
Returns text styles from the 100th to the last character:
parseTextStyle(textNode, 100)
Returns styles for the entire text, but only with fontName and textDecoration:
parseTextStyle(textNode, undefined, undefined, ["fontName", "textDecoration"])
*/
function parseTextStyle(
node: TextNode,
start = 0,
end?: number,
styleName?: FontStyleNames[]
): LetterStyle[] {
if (!end) end = node.characters.length
if (!styleName) styleName = styleFonts
if (end <= start) {
console.error('Start must be greater than end')
return []
}
// string substring, defined styles
const styleMap = []
// a composing string of a specific style
let textStyle: LetterStyle
const names = styleName.map((name) => {
return name.replace(/^(.)/g, ($1) => $1.toUpperCase())
})
// splitting text into substrings by style
for (let startIndex = start; startIndex < end; startIndex++) {
const endIndex = startIndex + 1
const letter = { characters: node.characters[startIndex] }
// collection of styles
names.forEach((n, i) => {
const rangeStyleValue = node['getRange' + n](startIndex, endIndex)
if (
n.toLowerCase() !== 'hyperlink' ||
(rangeStyleValue && rangeStyleValue.constructor === Object)
) {
letter[styleName[i]] = rangeStyleValue
}
})
if (textStyle) {
if (isEqualLetterStyle(letter, textStyle)) {
// the character has the same properties as the generated substring
// add it to it
textStyle.characters += letter.characters
} else {
// style properties are different
styleMap.push(textStyle)
// we start to form a new substring
textStyle = letter
}
} else {
// we start forming the first substring
textStyle = letter
}
}
styleMap.push(textStyle)
return styleMap
}
/*
Allows to split the styles obtained with parseTextStyle into lines based on newlines.
If the removeNewlineCharacters parameter == true, the newline characters will be removed.
RemoveEmptylines == true will remove empty lines.
*/
function splitTextStyleIntoLines(
textStyle: LetterStyle[],
removeNewlineCharacters = false,
removeEmptylines = false
) {
let line: LineStyle = []
let lines: LineStyle[] = []
const re = new RegExp('(.+|(?<=\n)(.?)(?=$))(\n|\u2028)?|(\n|\u2028)', 'g')
const re2 = new RegExp('\n|\u2028')
textStyle.forEach((style, index) => {
if (re2.test(style.characters)) {
const ls = style.characters.match(re)
if (ls === null) {
// text is missing
line.push(style)
} else if (ls.length === 1) {
// the style text consists of 1 line
line.push(style)
lines.push(line)
line = []
} else {
// multiple-line text
style = cloneDeep(style)
style.characters = ls.shift()
line.push(style)
lines.push(line)
line = []
const last = ls.pop()
// dealing with internal text strings
lines.push(
...ls.map((e) => {
style = cloneDeep(style)
style.characters = e
return [style]
})
)
style = cloneDeep(style)
style.characters = last
if (last === '') {
if (!textStyle[index + 1]) {
// last line final
lines.push([style])
} // else false end of text
} else {
// does not end
line.push(style)
}
}
} else {
line.push(style)
}
})
if (line.length) lines.push(line)
// deleting newline characters
if (removeNewlineCharacters) {
lines.forEach((l) => {
const style = l[l.length - 1]
style.characters = style.characters.replace(re2, '')
})
}
// deleting empty lines
if (removeEmptylines) {
lines = lines.filter(
(l) => l.filter((l) => l.characters.replace(re2, '') !== '').length !== 0
)
}
return lines
}
/*
Inverse function of splitTextStyleIntoLines.
The addNewlineCharacters parameter is responsible for whether you need to add a newline character at the end of each line
*/
function joinTextLinesStyles(
textStyle: LineStyle[],
addNewlineCharacters: boolean | '\n' | '\u2028' = false
) {
const tStyle = cloneDeep(textStyle)
let newline = ''
switch (typeof addNewlineCharacters) {
case 'boolean':
if (addNewlineCharacters) newline = '\n'
break
case 'string':
newline = addNewlineCharacters
break
}
// adding new line characters
if (addNewlineCharacters && newline) {
tStyle.forEach((style, i) => {
if (i !== tStyle.length - 1) style[style.length - 1].characters += newline
})
}
// join
const line = tStyle.shift()
tStyle.forEach((style) => {
const fitst = style.shift()
if (isEqualLetterStyle(fitst, line[line.length - 1])) {
// the style of the beginning of the line differs from the end of the style of the text being compiled
line[line.length - 1].characters += fitst.characters
} else {
line.push(fitst)
}
if (style.length) line.push(...style)
})
return line
}
/*
Apply the text styles obtained from parseTextStyle to the text node.
The second parameter can be passed a text node, the text of which will be changed.
*/
async function applyTextStyleToTextNode(
textStyle: LetterStyle[],
textNode?: TextNode,
isLoadFonts = true
) {
if (isLoadFonts) {
let fonts = [
{
family: 'Roboto',
style: 'Regular'
}
]
if (textStyle[0].fontName) {
fonts.push(...textStyle.map((e) => e.fontName))
}
if (textNode) {
fonts.push(...getAllFonts([textNode]))
}
fonts = uniqWith(fonts, isEqual)
await loadFonts(fonts)
}
if (!textNode) textNode = figma.createText()
textNode.characters = textStyle.reduce((str, style) => {
return str + style.characters
}, '')
let n = 0
textStyle.forEach((style) => {
const L = style.characters.length
if (L) {
for (const key in style) {
if (key !== 'characters') {
const name = key.replace(/^(.)/g, ($1) => $1.toUpperCase())
textNode['setRange' + name](n, n + L, style[key])
}
}
n += L
}
})
return textNode
}
/*
Replacing text in textStyle
If the passed text is shorter than in styles, the extra styles will be removed.
If the passed text is longer than the styles, the overflow text will get the style of the last character.
*/
function changeCharactersTextStyle(textStyle: LetterStyle[], characters: string) {
textStyle = cloneDeep(textStyle)
let n = 0
const length = textStyle.length - 1
const charactersLength = characters.length
for (let i = 0; i <= length; i++) {
const s = textStyle[i]
let l = s.characters.length
// if passed text is longer than text in styles
if (i == length) l = charactersLength
s.characters = characters.slice(n, n + l)
n += l
if (n > charactersLength) {
// new text is shorter than text in styles
textStyle = textStyle.splice(0, i + 1)
continue
}
}
return textStyle
}
/*
Function for changing properties of TextStyle.
The beforeValue parameter allows you to specify the value in which the property to be changed should be.
*/
function changeTextStyle(
textStyle: LetterStyle[],
styleName: 'fontSize',
newValue: number,
beforeValue?: number
)
function changeTextStyle(
textStyle: LetterStyle[],
styleName: 'fontName',
newValue: FontName,
beforeValue?: FontName
)
function changeTextStyle(
textStyle: LetterStyle[],
styleName: 'textCase',
newValue: TextCase,
beforeValue?: TextCase
)
function changeTextStyle(
textStyle: LetterStyle[],
styleName: 'textDecoration',
newValue: TextDecoration,
beforeValue?: TextDecoration
)
function changeTextStyle(
textStyle: LetterStyle[],
styleName: 'letterSpacing',
newValue: LetterSpacing,
beforeValue?: LetterSpacing
)
function changeTextStyle(
textStyle: LetterStyle[],
styleName: 'lineHeight',
newValue: LineHeight,
beforeValue?: LineHeight
)
function changeTextStyle(
textStyle: LetterStyle[],
styleName: 'fills',
newValue: Paint[],
beforeValue?: Paint[]
)
function changeTextStyle(
textStyle: LetterStyle[],
styleName: 'textStyleId' | 'fillStyleId',
newValue: string,
beforeValue?: string
)
function changeTextStyle(
textStyle: LetterStyle[],
styleName: FontStyleNames,
newValue: any,
beforeValue?: any
) {
textStyle = cloneDeep(textStyle)
textStyle.forEach((style) => {
if (
beforeValue === undefined ||
(beforeValue !== undefined && isEqual(style[styleName], beforeValue))
) {
;(style as any)[styleName] = newValue
}
})
return textStyle
}
/*comparing character styles to the styles of the composing substring*/
function isEqualLetterStyle(letter: LetterStyle, textStyle: LetterStyle): boolean {
let is = true
// iterating over font properties
for (const key in letter) {
if (key !== 'characters') {
if (!isEqual(letter[key], textStyle[key])) {
// property varies
// stop searching
is = false
break
}
}
}
return is
}
export {
parseTextStyle,
splitTextStyleIntoLines,
joinTextLinesStyles,
applyTextStyleToTextNode,
changeCharactersTextStyle,
changeTextStyle
}