@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.
- package/README.md +116 -199
- package/dist/cli.cjs +2780 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +2760 -0
- package/dist/cli.js.map +1 -0
- package/dist/relay.cjs +159 -0
- package/dist/relay.cjs.map +1 -0
- package/dist/relay.d.cts +4 -0
- package/dist/relay.d.ts +4 -0
- package/dist/relay.js +136 -0
- package/dist/relay.js.map +1 -0
- package/dist/talk_to_figma_mcp/server.cjs.map +1 -0
- package/dist/talk_to_figma_mcp/server.d.cts +1 -0
- package/dist/talk_to_figma_mcp/server.d.ts +1 -0
- package/dist/talk_to_figma_mcp/server.js.map +1 -0
- package/figma-plugin/code.js +4340 -0
- package/figma-plugin/figma-plugin.zip +0 -0
- package/figma-plugin/manifest.json +23 -0
- package/figma-plugin/setcharacters.js +215 -0
- package/figma-plugin/ui.html +834 -0
- package/package.json +5 -3
- package/dist/server.cjs.map +0 -1
- package/dist/server.js.map +0 -1
- /package/dist/{server.d.cts → cli.d.cts} +0 -0
- /package/dist/{server.d.ts → cli.d.ts} +0 -0
- /package/dist/{server.cjs → talk_to_figma_mcp/server.cjs} +0 -0
- /package/dist/{server.js → talk_to_figma_mcp/server.js} +0 -0
|
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
|
+
};
|