@biolab/talk-to-figma 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "Cursor MCP Plugin",
3
+ "id": "1485687494525374295",
4
+ "api": "1.0.0",
5
+ "main": "code.js",
6
+ "ui": "ui.html",
7
+ "editorType": [
8
+ "figma",
9
+ "figjam"
10
+ ],
11
+ "permissions": [],
12
+ "networkAccess": {
13
+ "allowedDomains": [
14
+ "ws://localhost:3055"
15
+ ],
16
+ "reasoning": "This is a plugin for Cursor that allows you to connect to a local server",
17
+ "devAllowedDomains": [
18
+ "http://localhost:3055",
19
+ "ws://localhost:3055"
20
+ ]
21
+ },
22
+ "documentAccess": "dynamic-page"
23
+ }
@@ -0,0 +1,215 @@
1
+ function uniqBy(arr, predicate) {
2
+ const cb = typeof predicate === "function" ? predicate : (o) => o[predicate];
3
+ return [
4
+ ...arr
5
+ .reduce((map, item) => {
6
+ const key = item === null || item === undefined ? item : cb(item);
7
+
8
+ map.has(key) || map.set(key, item);
9
+
10
+ return map;
11
+ }, new Map())
12
+ .values(),
13
+ ];
14
+ }
15
+ export const setCharacters = async (node, characters, options) => {
16
+ const fallbackFont = options?.fallbackFont || {
17
+ family: "Roboto",
18
+ style: "Regular",
19
+ };
20
+ try {
21
+ if (node.fontName === figma.mixed) {
22
+ if (options?.smartStrategy === "prevail") {
23
+ const fontHashTree = {};
24
+ for (let i = 1; i < node.characters.length; i++) {
25
+ const charFont = node.getRangeFontName(i - 1, i);
26
+ const key = `${charFont.family}::${charFont.style}`;
27
+ fontHashTree[key] = fontHashTree[key] ? fontHashTree[key] + 1 : 1;
28
+ }
29
+ const prevailedTreeItem = Object.entries(fontHashTree).sort(
30
+ (a, b) => b[1] - a[1]
31
+ )[0];
32
+ const [family, style] = prevailedTreeItem[0].split("::");
33
+ const prevailedFont = {
34
+ family,
35
+ style,
36
+ };
37
+ await figma.loadFontAsync(prevailedFont);
38
+ node.fontName = prevailedFont;
39
+ } else if (options?.smartStrategy === "strict") {
40
+ return setCharactersWithStrictMatchFont(node, characters, fallbackFont);
41
+ } else if (options?.smartStrategy === "experimental") {
42
+ return setCharactersWithSmartMatchFont(node, characters, fallbackFont);
43
+ } else {
44
+ const firstCharFont = node.getRangeFontName(0, 1);
45
+ await figma.loadFontAsync(firstCharFont);
46
+ node.fontName = firstCharFont;
47
+ }
48
+ } else {
49
+ await figma.loadFontAsync({
50
+ family: node.fontName.family,
51
+ style: node.fontName.style,
52
+ });
53
+ }
54
+ } catch (err) {
55
+ console.warn(
56
+ `Failed to load "${node.fontName["family"]} ${node.fontName["style"]}" font and replaced with fallback "${fallbackFont.family} ${fallbackFont.style}"`,
57
+ err
58
+ );
59
+ await figma.loadFontAsync(fallbackFont);
60
+ node.fontName = fallbackFont;
61
+ }
62
+ try {
63
+ node.characters = characters;
64
+ return true;
65
+ } catch (err) {
66
+ console.warn(`Failed to set characters. Skipped.`, err);
67
+ return false;
68
+ }
69
+ };
70
+
71
+ const setCharactersWithStrictMatchFont = async (
72
+ node,
73
+ characters,
74
+ fallbackFont
75
+ ) => {
76
+ const fontHashTree = {};
77
+ for (let i = 1; i < node.characters.length; i++) {
78
+ const startIdx = i - 1;
79
+ const startCharFont = node.getRangeFontName(startIdx, i);
80
+ const startCharFontVal = `${startCharFont.family}::${startCharFont.style}`;
81
+ while (i < node.characters.length) {
82
+ i++;
83
+ const charFont = node.getRangeFontName(i - 1, i);
84
+ if (startCharFontVal !== `${charFont.family}::${charFont.style}`) {
85
+ break;
86
+ }
87
+ }
88
+ fontHashTree[`${startIdx}_${i}`] = startCharFontVal;
89
+ }
90
+ await figma.loadFontAsync(fallbackFont);
91
+ node.fontName = fallbackFont;
92
+ node.characters = characters;
93
+ console.log(fontHashTree);
94
+ await Promise.all(
95
+ Object.keys(fontHashTree).map(async (range) => {
96
+ console.log(range, fontHashTree[range]);
97
+ const [start, end] = range.split("_");
98
+ const [family, style] = fontHashTree[range].split("::");
99
+ const matchedFont = {
100
+ family,
101
+ style,
102
+ };
103
+ await figma.loadFontAsync(matchedFont);
104
+ return node.setRangeFontName(Number(start), Number(end), matchedFont);
105
+ })
106
+ );
107
+ return true;
108
+ };
109
+
110
+ const getDelimiterPos = (str, delimiter, startIdx = 0, endIdx = str.length) => {
111
+ const indices = [];
112
+ let temp = startIdx;
113
+ for (let i = startIdx; i < endIdx; i++) {
114
+ if (
115
+ str[i] === delimiter &&
116
+ i + startIdx !== endIdx &&
117
+ temp !== i + startIdx
118
+ ) {
119
+ indices.push([temp, i + startIdx]);
120
+ temp = i + startIdx + 1;
121
+ }
122
+ }
123
+ temp !== endIdx && indices.push([temp, endIdx]);
124
+ return indices.filter(Boolean);
125
+ };
126
+
127
+ const buildLinearOrder = (node) => {
128
+ const fontTree = [];
129
+ const newLinesPos = getDelimiterPos(node.characters, "\n");
130
+ newLinesPos.forEach(([newLinesRangeStart, newLinesRangeEnd], n) => {
131
+ const newLinesRangeFont = node.getRangeFontName(
132
+ newLinesRangeStart,
133
+ newLinesRangeEnd
134
+ );
135
+ if (newLinesRangeFont === figma.mixed) {
136
+ const spacesPos = getDelimiterPos(
137
+ node.characters,
138
+ " ",
139
+ newLinesRangeStart,
140
+ newLinesRangeEnd
141
+ );
142
+ spacesPos.forEach(([spacesRangeStart, spacesRangeEnd], s) => {
143
+ const spacesRangeFont = node.getRangeFontName(
144
+ spacesRangeStart,
145
+ spacesRangeEnd
146
+ );
147
+ if (spacesRangeFont === figma.mixed) {
148
+ const spacesRangeFont = node.getRangeFontName(
149
+ spacesRangeStart,
150
+ spacesRangeStart[0]
151
+ );
152
+ fontTree.push({
153
+ start: spacesRangeStart,
154
+ delimiter: " ",
155
+ family: spacesRangeFont.family,
156
+ style: spacesRangeFont.style,
157
+ });
158
+ } else {
159
+ fontTree.push({
160
+ start: spacesRangeStart,
161
+ delimiter: " ",
162
+ family: spacesRangeFont.family,
163
+ style: spacesRangeFont.style,
164
+ });
165
+ }
166
+ });
167
+ } else {
168
+ fontTree.push({
169
+ start: newLinesRangeStart,
170
+ delimiter: "\n",
171
+ family: newLinesRangeFont.family,
172
+ style: newLinesRangeFont.style,
173
+ });
174
+ }
175
+ });
176
+ return fontTree
177
+ .sort((a, b) => +a.start - +b.start)
178
+ .map(({ family, style, delimiter }) => ({ family, style, delimiter }));
179
+ };
180
+
181
+ const setCharactersWithSmartMatchFont = async (
182
+ node,
183
+ characters,
184
+ fallbackFont
185
+ ) => {
186
+ const rangeTree = buildLinearOrder(node);
187
+ const fontsToLoad = uniqBy(
188
+ rangeTree,
189
+ ({ family, style }) => `${family}::${style}`
190
+ ).map(({ family, style }) => ({
191
+ family,
192
+ style,
193
+ }));
194
+
195
+ await Promise.all([...fontsToLoad, fallbackFont].map(figma.loadFontAsync));
196
+
197
+ node.fontName = fallbackFont;
198
+ node.characters = characters;
199
+
200
+ let prevPos = 0;
201
+ rangeTree.forEach(({ family, style, delimiter }) => {
202
+ if (prevPos < node.characters.length) {
203
+ const delimeterPos = node.characters.indexOf(delimiter, prevPos);
204
+ const endPos =
205
+ delimeterPos > prevPos ? delimeterPos : node.characters.length;
206
+ const matchedFont = {
207
+ family,
208
+ style,
209
+ };
210
+ node.setRangeFontName(prevPos, endPos, matchedFont);
211
+ prevPos = endPos + 1;
212
+ }
213
+ });
214
+ return true;
215
+ };