flowfig 0.6.0 → 0.8.0
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 +28 -8
- package/dist/browser.d.ts +1 -3
- package/dist/browser.js +67 -59
- package/dist/check.d.ts +5 -3
- package/dist/check.js +24 -17
- package/dist/cli.js +43 -49
- package/dist/code.d.ts +26 -0
- package/dist/code.js +568 -0
- package/dist/diff.js +0 -4
- package/dist/draw.d.ts +1 -3
- package/dist/draw.js +3 -7
- package/dist/edges.d.ts +7 -0
- package/dist/edges.js +510 -0
- package/dist/geometry.d.ts +6 -8
- package/dist/geometry.js +75 -82
- package/dist/gif.d.ts +1 -2
- package/dist/gif.js +115 -42
- package/dist/guide.d.ts +1 -4
- package/dist/guide.js +10 -11
- package/dist/imports.d.ts +11 -0
- package/dist/imports.js +235 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +20 -78
- package/dist/init.d.ts +3 -13
- package/dist/init.js +13 -18
- package/dist/load.d.ts +1 -3
- package/dist/load.js +2 -5
- package/dist/mcp.d.ts +1 -2
- package/dist/mcp.js +15 -15
- package/dist/model.d.ts +57 -109
- package/dist/model.js +20 -89
- package/dist/open.d.ts +2 -5
- package/dist/open.js +6 -11
- package/dist/page.d.ts +0 -1
- package/dist/page.js +1 -3
- package/dist/picker.d.ts +2 -4
- package/dist/picker.js +11 -14
- package/dist/png.js +9 -10
- package/dist/rail.d.ts +1 -3
- package/dist/rail.js +2 -16
- package/dist/scene.d.ts +8 -8
- package/dist/source.d.ts +1 -2
- package/dist/source.js +5 -5
- package/dist/svg.d.ts +5 -8
- package/dist/svg.js +50 -112
- package/dist/text.d.ts +0 -2
- package/dist/text.js +1 -4
- package/dist/type-index.d.ts +55 -0
- package/dist/type-index.js +383 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +733 -0
- package/dist/verify.d.ts +26 -4
- package/dist/verify.js +68 -26
- package/dist/version.d.ts +1 -2
- package/dist/version.js +2 -2
- package/package.json +4 -2
package/dist/code.js
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
const LANGS = {
|
|
4
|
+
'.ts': 'ts',
|
|
5
|
+
'.tsx': 'ts',
|
|
6
|
+
'.mts': 'ts',
|
|
7
|
+
'.cts': 'ts',
|
|
8
|
+
'.js': 'ts',
|
|
9
|
+
'.jsx': 'ts',
|
|
10
|
+
'.mjs': 'ts',
|
|
11
|
+
'.cjs': 'ts',
|
|
12
|
+
'.py': 'py',
|
|
13
|
+
'.go': 'go',
|
|
14
|
+
'.java': 'java',
|
|
15
|
+
'.cs': 'cs',
|
|
16
|
+
'.rs': 'rs',
|
|
17
|
+
};
|
|
18
|
+
export const langOf = (path) => LANGS[extname(path)] ?? null;
|
|
19
|
+
export const readFile = (full) => {
|
|
20
|
+
try {
|
|
21
|
+
return statSync(full).isFile() ? readFileSync(full, 'utf8') : null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
readFile.files = (root) => {
|
|
28
|
+
const out = [];
|
|
29
|
+
const walk = (dir) => {
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const e of entries) {
|
|
38
|
+
if (e.name.startsWith('.') || e.name === 'node_modules')
|
|
39
|
+
continue;
|
|
40
|
+
const full = resolve(dir, e.name);
|
|
41
|
+
if (e.isDirectory())
|
|
42
|
+
walk(full);
|
|
43
|
+
else
|
|
44
|
+
out.push(relative(root, full).split(sep).join('/'));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
walk(root);
|
|
48
|
+
return out;
|
|
49
|
+
};
|
|
50
|
+
const blank = (s) => s.replace(/[^\n]/g, ' ');
|
|
51
|
+
const wordBefore = (s, end) => /(\w+)$/.exec(s.slice(Math.max(0, end - 12), end))?.[1] ?? '';
|
|
52
|
+
function mask(src, lang) {
|
|
53
|
+
let code = '';
|
|
54
|
+
let keep = '';
|
|
55
|
+
const n = src.length;
|
|
56
|
+
let i = 0;
|
|
57
|
+
const push = (txt, kind) => {
|
|
58
|
+
code += kind === 'code' ? txt : blank(txt);
|
|
59
|
+
keep += kind === 'comment' ? blank(txt) : txt;
|
|
60
|
+
};
|
|
61
|
+
if (lang === 'py') {
|
|
62
|
+
while (i < n) {
|
|
63
|
+
const c = src[i];
|
|
64
|
+
if (c === '#') {
|
|
65
|
+
const e = src.indexOf('\n', i);
|
|
66
|
+
const end = e < 0 ? n : e;
|
|
67
|
+
push(src.slice(i, end), 'comment');
|
|
68
|
+
i = end;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const m = /^([rRbBuUfF]{0,2})('''|"""|'|")/.exec(src.slice(i, i + 5));
|
|
72
|
+
if (m && (m[1] === '' || !/[\w]/.test(src[i - 1] ?? ''))) {
|
|
73
|
+
const q = m[2];
|
|
74
|
+
let j = i + m[0].length;
|
|
75
|
+
while (j < n) {
|
|
76
|
+
if (src[j] === '\\')
|
|
77
|
+
j += 2;
|
|
78
|
+
else if (src.startsWith(q, j)) {
|
|
79
|
+
j += q.length;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
else if (q.length === 1 && src[j] === '\n')
|
|
83
|
+
break;
|
|
84
|
+
else
|
|
85
|
+
j++;
|
|
86
|
+
}
|
|
87
|
+
const bodyAt = i + m[0].length;
|
|
88
|
+
const closeAt = Math.max(bodyAt, j - q.length);
|
|
89
|
+
push(src.slice(i, bodyAt), 'code');
|
|
90
|
+
push(src.slice(bodyAt, closeAt), 'string');
|
|
91
|
+
push(src.slice(closeAt, j), 'code');
|
|
92
|
+
i = j;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
push(c, 'code');
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
return { code, keep };
|
|
99
|
+
}
|
|
100
|
+
const stack = [];
|
|
101
|
+
let depth = 0;
|
|
102
|
+
let prevSig = '';
|
|
103
|
+
while (i < n) {
|
|
104
|
+
const c = src[i];
|
|
105
|
+
const d = src[i + 1];
|
|
106
|
+
if (c === '/' && d === '/') {
|
|
107
|
+
const e = src.indexOf('\n', i);
|
|
108
|
+
const end = e < 0 ? n : e;
|
|
109
|
+
push(src.slice(i, end), 'comment');
|
|
110
|
+
i = end;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (c === '/' && d === '*') {
|
|
114
|
+
const e = src.indexOf('*/', i + 2);
|
|
115
|
+
const end = e < 0 ? n : e + 2;
|
|
116
|
+
push(src.slice(i, end), 'comment');
|
|
117
|
+
i = end;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (lang === 'ts' && c === '/' && /^$|[(,=:[!&|?{};]$/.test(prevSig)) {
|
|
121
|
+
let j = i + 1;
|
|
122
|
+
let cls = false;
|
|
123
|
+
while (j < n && src[j] !== '\n') {
|
|
124
|
+
if (src[j] === '\\')
|
|
125
|
+
j += 2;
|
|
126
|
+
else if (src[j] === '/' && !cls)
|
|
127
|
+
break;
|
|
128
|
+
else {
|
|
129
|
+
if (src[j] === '[')
|
|
130
|
+
cls = true;
|
|
131
|
+
else if (src[j] === ']')
|
|
132
|
+
cls = false;
|
|
133
|
+
j++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (src[j] === '/') {
|
|
137
|
+
push('/', 'code');
|
|
138
|
+
push(src.slice(i + 1, j), 'string');
|
|
139
|
+
push('/', 'code');
|
|
140
|
+
i = j + 1;
|
|
141
|
+
prevSig = 'x';
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (lang === 'rs' && c === "'" && /[A-Za-z_]/.test(d ?? '') && src[i + 2] !== "'") {
|
|
146
|
+
push(c, 'code');
|
|
147
|
+
i++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const tpl = lang === 'ts' && c === '`';
|
|
151
|
+
const raw = (lang === 'go' && c === '`') || (lang === 'cs' && c === '@' && d === '"');
|
|
152
|
+
if (c === '"' || c === "'" || tpl || raw || (c === '}' && stack.length && stack.at(-1) === depth)) {
|
|
153
|
+
let open = c;
|
|
154
|
+
if (c === '}') {
|
|
155
|
+
stack.pop();
|
|
156
|
+
depth--;
|
|
157
|
+
}
|
|
158
|
+
if (raw && c === '@')
|
|
159
|
+
open = '"';
|
|
160
|
+
const startQ = c === '@' ? i + 2 : i + 1;
|
|
161
|
+
const q = c === '}' ? '`' : open;
|
|
162
|
+
const triple = (lang === 'java' || lang === 'cs') && src.startsWith('"""', i);
|
|
163
|
+
let j = triple ? i + 3 : startQ;
|
|
164
|
+
let enterTpl = false;
|
|
165
|
+
while (j < n) {
|
|
166
|
+
if (triple) {
|
|
167
|
+
if (src.startsWith('"""', j))
|
|
168
|
+
break;
|
|
169
|
+
j++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (src[j] === '\\' && !raw)
|
|
173
|
+
j += 2;
|
|
174
|
+
else if (raw && c === '@' && src[j] === '"' && src[j + 1] === '"')
|
|
175
|
+
j += 2;
|
|
176
|
+
else if (src[j] === q)
|
|
177
|
+
break;
|
|
178
|
+
else if (q === '`' && lang === 'ts' && src[j] === '$' && src[j + 1] === '{') {
|
|
179
|
+
enterTpl = true;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
else if (!raw && q !== '`' && src[j] === '\n')
|
|
183
|
+
break;
|
|
184
|
+
else
|
|
185
|
+
j++;
|
|
186
|
+
}
|
|
187
|
+
const head = c === '}' ? 1 : startQ - i;
|
|
188
|
+
push(src.slice(i, i + head), 'code');
|
|
189
|
+
push(src.slice(i + head, j), 'string');
|
|
190
|
+
if (enterTpl) {
|
|
191
|
+
push('${', 'code');
|
|
192
|
+
depth++;
|
|
193
|
+
stack.push(depth);
|
|
194
|
+
i = j + 2;
|
|
195
|
+
prevSig = '{';
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const tail = triple ? 3 : 1;
|
|
199
|
+
push(src.slice(j, j + tail), 'code');
|
|
200
|
+
i = j + tail;
|
|
201
|
+
prevSig = 'x';
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (c === '{')
|
|
205
|
+
depth++;
|
|
206
|
+
else if (c === '}')
|
|
207
|
+
depth--;
|
|
208
|
+
push(c, 'code');
|
|
209
|
+
if (!/\s/.test(c))
|
|
210
|
+
prevSig = /\w/.test(c)
|
|
211
|
+
? !/\w/.test(src[i + 1] ?? '') && /^(return|typeof|case|in|of)$/.test(wordBefore(src, i + 1))
|
|
212
|
+
? '('
|
|
213
|
+
: 'x'
|
|
214
|
+
: c;
|
|
215
|
+
i++;
|
|
216
|
+
}
|
|
217
|
+
return { code, keep };
|
|
218
|
+
}
|
|
219
|
+
export const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
220
|
+
export function matchClose(code, open) {
|
|
221
|
+
const pair = { '{': '}', '(': ')', '[': ']' }[code[open]];
|
|
222
|
+
let d = 0;
|
|
223
|
+
for (let i = open; i < code.length; i++) {
|
|
224
|
+
if (code[i] === code[open])
|
|
225
|
+
d++;
|
|
226
|
+
else if (code[i] === pair && --d === 0)
|
|
227
|
+
return i;
|
|
228
|
+
}
|
|
229
|
+
return code.length - 1;
|
|
230
|
+
}
|
|
231
|
+
const indentAt = (code, pos) => {
|
|
232
|
+
const ls = code.lastIndexOf('\n', pos - 1) + 1;
|
|
233
|
+
return /^[ \t]*/.exec(code.slice(ls))[0].length;
|
|
234
|
+
};
|
|
235
|
+
function braceBody(code, from, start, lang) {
|
|
236
|
+
let i = from;
|
|
237
|
+
let pd = 0;
|
|
238
|
+
const defIndent = indentAt(code, start);
|
|
239
|
+
while (i < code.length) {
|
|
240
|
+
const c = code[i];
|
|
241
|
+
if (c === '(' || c === '[')
|
|
242
|
+
pd++;
|
|
243
|
+
else if (c === ')' || c === ']') {
|
|
244
|
+
if (--pd < 0)
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
else if (pd === 0) {
|
|
248
|
+
if (c === ';')
|
|
249
|
+
return [start, i + 1];
|
|
250
|
+
if (c === '\n' && lang === 'go' && !/[,({[=+\-*/|&.:]\s*$/.test(code.slice(Math.max(start, i - 40), i)))
|
|
251
|
+
return [start, i];
|
|
252
|
+
if (c === '}')
|
|
253
|
+
return null;
|
|
254
|
+
if (c === '=' && code[i + 1] === '>' && lang === 'ts') {
|
|
255
|
+
let k = i + 2;
|
|
256
|
+
while (/\s/.test(code[k]))
|
|
257
|
+
k++;
|
|
258
|
+
if (code[k] !== '{') {
|
|
259
|
+
let pd2 = 0;
|
|
260
|
+
for (let j = k; j < code.length; j++) {
|
|
261
|
+
const ch = code[j];
|
|
262
|
+
if ('([{'.includes(ch))
|
|
263
|
+
pd2++;
|
|
264
|
+
else if (')]}'.includes(ch)) {
|
|
265
|
+
if (--pd2 < 0)
|
|
266
|
+
return [start, j];
|
|
267
|
+
}
|
|
268
|
+
else if (pd2 === 0 && (ch === ';' || ch === ','))
|
|
269
|
+
return [start, j];
|
|
270
|
+
else if (pd2 === 0 && ch === '\n') {
|
|
271
|
+
const next = code.slice(j + 1).match(/^[ \t]*(\S)/);
|
|
272
|
+
if (next && indentAt(code, j + 1) <= defIndent && !/[.?:|&+]/.test(next[1]))
|
|
273
|
+
return [start, j];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return [start, code.length];
|
|
277
|
+
}
|
|
278
|
+
i = k;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (c === '{') {
|
|
282
|
+
const before = code.slice(Math.max(0, i - 12), i).trimEnd();
|
|
283
|
+
const goTypeDecl = lang === 'go' && /^type\s+\w+(\[[^\]]*\])?\s+(struct|interface)\s*$/.test(code.slice(start, i));
|
|
284
|
+
if (!goTypeDecl && (/[:|&<,]$/.test(before) || /\b(interface|struct)$/.test(before))) {
|
|
285
|
+
i = matchClose(code, i) + 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
return [start, matchClose(code, i) + 1];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
i++;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
export function pyBody(code, start) {
|
|
296
|
+
const ind = indentAt(code, start);
|
|
297
|
+
let i = start;
|
|
298
|
+
let pd = 0;
|
|
299
|
+
for (; i < code.length; i++) {
|
|
300
|
+
const c = code[i];
|
|
301
|
+
if ('([{'.includes(c))
|
|
302
|
+
pd++;
|
|
303
|
+
else if (')]}'.includes(c))
|
|
304
|
+
pd--;
|
|
305
|
+
else if (c === ':' && pd === 0 && /^[ \t]*(\n|$)/.test(code.slice(i + 1, i + 200)))
|
|
306
|
+
break;
|
|
307
|
+
else if (c === '\n' && pd === 0 && !code.slice(start, i).endsWith('\\')) {
|
|
308
|
+
if (!/^\s*(async\s+)?(def|class)\b/.test(code.slice(code.lastIndexOf('\n', start - 1) + 1)))
|
|
309
|
+
return [start, i];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
let j = code.indexOf('\n', i);
|
|
313
|
+
if (j < 0)
|
|
314
|
+
return [start, code.length];
|
|
315
|
+
let end = j;
|
|
316
|
+
while (j < code.length) {
|
|
317
|
+
const ls = j + 1;
|
|
318
|
+
const le = code.indexOf('\n', ls) < 0 ? code.length : code.indexOf('\n', ls);
|
|
319
|
+
const line = code.slice(ls, le);
|
|
320
|
+
if (line.trim() && indentAt(code, ls) <= ind)
|
|
321
|
+
break;
|
|
322
|
+
if (line.trim())
|
|
323
|
+
end = le;
|
|
324
|
+
j = le;
|
|
325
|
+
if (le >= code.length)
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
return [start, end];
|
|
329
|
+
}
|
|
330
|
+
const E = '(?![\\p{L}\\p{N}_$])';
|
|
331
|
+
function defPatterns(lang, name, inContainer) {
|
|
332
|
+
const N = esc(name);
|
|
333
|
+
const re = (src) => new RegExp(src, 'gu');
|
|
334
|
+
switch (lang) {
|
|
335
|
+
case 'ts':
|
|
336
|
+
return [
|
|
337
|
+
re(`\\bfunction\\s*\\*?\\s*${N}\\s*[<(]`),
|
|
338
|
+
re(`\\b(class|interface|enum|namespace|module)\\s+${N}${E}`),
|
|
339
|
+
re(`\\btype\\s+${N}${E}\\s*[<=]`),
|
|
340
|
+
re(`\\b(const|let|var)\\s+${N}${E}\\s*[:=]`),
|
|
341
|
+
re(`(^|[\\n;{}])[ \\t]*((public|private|protected|static|async|readonly|override|abstract|get|set|declare)\\s+)*\\*?${N}\\s*[<(]`),
|
|
342
|
+
re(`(^|[\\n;{}])[ \\t]*((public|private|protected|static|readonly|override|declare)\\s+)*${N}\\s*[?!]?\\s*(:(?:[^=;\\n]|=>)*)?=(?![=>])`),
|
|
343
|
+
re(`(^|[\\n;{}])[ \\t]*(module\\.)?exports\\.${N}\\s*=(?!=)`),
|
|
344
|
+
...(inContainer ? [re(`(^|[\\n{,])[ \\t]*${N}\\s*[?]?:`)] : []),
|
|
345
|
+
];
|
|
346
|
+
case 'py':
|
|
347
|
+
return [
|
|
348
|
+
re(`(^|\\n)[ \\t]*(async\\s+)?def\\s+${N}\\s*\\(`),
|
|
349
|
+
re(`(^|\\n)[ \\t]*class\\s+${N}${E}`),
|
|
350
|
+
re(`(^|\\n)${inContainer ? '[ \\t]+' : ''}${N}\\s*(:[^=\\n]+)?=(?!=)`),
|
|
351
|
+
];
|
|
352
|
+
case 'go':
|
|
353
|
+
return [
|
|
354
|
+
re(`\\bfunc\\s+${N}\\s*[\\[(]`),
|
|
355
|
+
re(`\\bfunc\\s*\\([^)]*\\)\\s*${N}\\s*[\\[(]`),
|
|
356
|
+
re(`\\btype\\s+${N}${E}`),
|
|
357
|
+
re(`\\b(var|const)\\s+${N}${E}`),
|
|
358
|
+
re(`(^|\\n)[ \\t]+${N}${E}(\\s*,\\s*[\\p{L}\\p{N}_]+)*\\s*(=|[\\w\\[*]+[^\\n]*=)`),
|
|
359
|
+
re(`(^|\\n)[ \\t]+${N}\\s+(struct|interface)${E}`),
|
|
360
|
+
...(inContainer ? [re(`(^|\\n)[ \\t]+${N}\\s*(\\(|\\s+\\S)`)] : []),
|
|
361
|
+
];
|
|
362
|
+
case 'java':
|
|
363
|
+
case 'cs':
|
|
364
|
+
return [
|
|
365
|
+
re(`\\b(class|interface|enum|record|struct)\\s+${N}${E}`),
|
|
366
|
+
re(`(^|[\\n;{}])[ \\t]*([\\w<>\\[\\],.?]+\\s+){1,12}${N}\\s*(<[^>]*>)?\\s*\\(`),
|
|
367
|
+
re(`(^|[\\n;{}\\]])[ \\t]*([\\w<>\\[\\],.?]+\\s+){1,12}${N}\\s*(=(?![=>])|;|\\{\\s*(get|set|init)\\b|=>)`),
|
|
368
|
+
];
|
|
369
|
+
case 'rs':
|
|
370
|
+
return [re(`\\bfn\\s+${N}${E}`), re(`\\b(struct|enum|trait|type|mod|const|static|union)\\s+${N}${E}`)];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'catch', 'return', 'new', 'await', 'typeof', 'else', 'do', 'throw']);
|
|
374
|
+
function depthAt(code, pos, lo) {
|
|
375
|
+
let d = 0;
|
|
376
|
+
for (let i = lo; i < pos; i++) {
|
|
377
|
+
if (code[i] === '{')
|
|
378
|
+
d++;
|
|
379
|
+
else if (code[i] === '}')
|
|
380
|
+
d--;
|
|
381
|
+
}
|
|
382
|
+
return d;
|
|
383
|
+
}
|
|
384
|
+
function inTypeBody(code, pos) {
|
|
385
|
+
for (let i = pos, d = 0; i >= 0; i--) {
|
|
386
|
+
if (code[i] === '}')
|
|
387
|
+
d++;
|
|
388
|
+
else if (code[i] === '{' && d-- === 0)
|
|
389
|
+
return /(?<![:,]\s*)\b(class|interface|enum|record|struct)\s+\w[^;{}]*$/.test(code.slice(Math.max(0, i - 300), i));
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
function pyMemberIndent(code, lo, hi) {
|
|
394
|
+
const base = indentAt(code, lo);
|
|
395
|
+
let min = Infinity;
|
|
396
|
+
for (const line of code.slice(lo, hi).split('\n').slice(1)) {
|
|
397
|
+
const ind = /^[ \t]*/.exec(line)[0].length;
|
|
398
|
+
if (line.trim() && ind > base)
|
|
399
|
+
min = Math.min(min, ind);
|
|
400
|
+
}
|
|
401
|
+
return min;
|
|
402
|
+
}
|
|
403
|
+
function findIn(code, lang, name, lo, hi, member, receiver) {
|
|
404
|
+
if (KEYWORDS.has(name) && !/^(rs|py|go)$/.test(lang))
|
|
405
|
+
return null;
|
|
406
|
+
const inContainer = member != null;
|
|
407
|
+
const memberIndent = inContainer && lang === 'py' ? pyMemberIndent(code, lo, hi) : -1;
|
|
408
|
+
const hits = [];
|
|
409
|
+
const pats = defPatterns(lang, name, inContainer);
|
|
410
|
+
for (const re of pats) {
|
|
411
|
+
re.lastIndex = lo;
|
|
412
|
+
let m;
|
|
413
|
+
while ((m = re.exec(code)) && m.index < hi) {
|
|
414
|
+
const nameAt = m.index + m[0].lastIndexOf(name);
|
|
415
|
+
const start = m.index + /^[;{}\]]?\s*/.exec(m[0])[0].length;
|
|
416
|
+
if (lang === 'go' && receiver != null && !/^\s*func\s*\(/.test(m[0]))
|
|
417
|
+
continue;
|
|
418
|
+
if (lang === 'go' && receiver != null && /^func\s*\(/.test(m[0])) {
|
|
419
|
+
const recv = /^func\s*\(([^)]*)\)/
|
|
420
|
+
.exec(m[0])[1]
|
|
421
|
+
.trim()
|
|
422
|
+
.replace(/^\w+\s+/, '');
|
|
423
|
+
if (!new RegExp(`^\\*?\\s*${esc(receiver)}(\\[[^\\]]*\\])?$`).test(recv))
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (code[nameAt - 1] === '.' && !/\bexports\.$/.test(code.slice(nameAt - 8, nameAt)))
|
|
427
|
+
continue;
|
|
428
|
+
if (lang === 'ts') {
|
|
429
|
+
const before = code.slice(0, start);
|
|
430
|
+
if (/[?:=&|]\s*$/.test(before) && !/\b(case\b[^:\n]*|default)\s*:\s*$/.test(before))
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (lang === 'go' && !inContainer && m[0].includes('=') && depthAt(code, start, 0) > 0)
|
|
434
|
+
continue;
|
|
435
|
+
if ((lang === 'java' || lang === 'cs') && re === pats.at(-1) && !inTypeBody(code, start))
|
|
436
|
+
continue;
|
|
437
|
+
const ext = lang === 'py' ? pyBody(code, start) : braceBody(code, nameAt + name.length, start, lang);
|
|
438
|
+
if (!ext)
|
|
439
|
+
continue;
|
|
440
|
+
const isMethodish = /\(\s*$/.test(m[0]) && !/\b(function|func|fn|def)\b/.test(m[0]);
|
|
441
|
+
if (isMethodish && lang !== 'py' && !(lang === 'go' && inContainer)) {
|
|
442
|
+
const p = code.indexOf('(', nameAt);
|
|
443
|
+
const close = matchClose(code, p);
|
|
444
|
+
const after = code.slice(close + 1).match(/^\s*(\S)/)?.[1];
|
|
445
|
+
if (!after ||
|
|
446
|
+
!(after === '{' ||
|
|
447
|
+
after === ':' ||
|
|
448
|
+
(lang !== 'ts' && /[\w=]/.test(after)) ||
|
|
449
|
+
(after === ';' && (lang === 'java' || lang === 'cs'))))
|
|
450
|
+
continue;
|
|
451
|
+
if (lang === 'java' || lang === 'cs') {
|
|
452
|
+
const pre = code.slice(start, nameAt);
|
|
453
|
+
if (/\b(new|return|else|throw)\s*$/.test(pre) || /[=.]\s*$/.test(pre))
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (lang === 'java' || lang === 'cs') {
|
|
458
|
+
const pre = code.slice(start, nameAt);
|
|
459
|
+
if (/\b(new|return|else|throw|await|yield|case|in|is|as|out|ref|goto|var|using|do)\s*$/.test(pre) || /[=.]\s*$/.test(pre))
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const depth = depthAt(code, start, lo);
|
|
463
|
+
if (inContainer && lang === 'py' ? indentAt(code, start) !== memberIndent : member != null && depth !== member)
|
|
464
|
+
continue;
|
|
465
|
+
hits.push({ start, end: ext[1], depth });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (!hits.length)
|
|
469
|
+
return null;
|
|
470
|
+
hits.sort((a, b) => a.depth - b.depth || a.start - b.start);
|
|
471
|
+
return [hits[0].start, hits[0].end];
|
|
472
|
+
}
|
|
473
|
+
function rustImpls(code, type) {
|
|
474
|
+
const out = [];
|
|
475
|
+
const self = new RegExp(`^(&\\s*('\\w+\\s+)?(mut\\s+)?)?(\\w+::)*${esc(type)}\\s*(<.*>)?$`, 's');
|
|
476
|
+
const re = /\bimpl\b([^{;]*)\{/g;
|
|
477
|
+
let m;
|
|
478
|
+
while ((m = re.exec(code))) {
|
|
479
|
+
let head = m[1].replace(/\bwhere\b[^]*$/, '').trim();
|
|
480
|
+
if (head.startsWith('<')) {
|
|
481
|
+
let d = 0;
|
|
482
|
+
let k = 0;
|
|
483
|
+
do {
|
|
484
|
+
if (head[k] === '<')
|
|
485
|
+
d++;
|
|
486
|
+
else if (head[k] === '>' && head[k - 1] !== '-')
|
|
487
|
+
d--;
|
|
488
|
+
} while (d > 0 && ++k < head.length);
|
|
489
|
+
head = head.slice(k + 1).trim();
|
|
490
|
+
}
|
|
491
|
+
const forAt = head.search(/\bfor\s/);
|
|
492
|
+
if (!self.test(forAt < 0 ? head : head.slice(forAt + 3).trim()))
|
|
493
|
+
continue;
|
|
494
|
+
const open = m.index + m[0].length - 1;
|
|
495
|
+
out.push([open, matchClose(code, open)]);
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
export function outside(root, path) {
|
|
500
|
+
const rel = relative(root, resolve(root, path));
|
|
501
|
+
return isAbsolute(path) || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
|
502
|
+
}
|
|
503
|
+
export function codeFile(root, path, read = readFile, cache = new Map()) {
|
|
504
|
+
const full = resolve(root, path);
|
|
505
|
+
if (cache.has(full))
|
|
506
|
+
return cache.get(full);
|
|
507
|
+
const lang = outside(root, path) ? null : langOf(path);
|
|
508
|
+
const raw = lang ? read(full) : null;
|
|
509
|
+
const text = raw?.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
510
|
+
const file = lang && text != null ? { path, lang, text, ...mask(text, lang) } : null;
|
|
511
|
+
cache.set(full, file);
|
|
512
|
+
return file;
|
|
513
|
+
}
|
|
514
|
+
export function locate(file, symbol) {
|
|
515
|
+
const { code, lang } = file;
|
|
516
|
+
const parts = symbol.split('.');
|
|
517
|
+
if (!parts.every((p) => /^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(p)))
|
|
518
|
+
return null;
|
|
519
|
+
const hit = (r) => {
|
|
520
|
+
if (!r)
|
|
521
|
+
return null;
|
|
522
|
+
let start = r[0];
|
|
523
|
+
for (;;) {
|
|
524
|
+
const prevEnd = code.lastIndexOf('\n', start - 1);
|
|
525
|
+
if (prevEnd < 0)
|
|
526
|
+
break;
|
|
527
|
+
const prevStart = code.lastIndexOf('\n', prevEnd - 1) + 1;
|
|
528
|
+
if (!/^\s*@/.test(code.slice(prevStart, prevEnd)))
|
|
529
|
+
break;
|
|
530
|
+
start = prevStart;
|
|
531
|
+
}
|
|
532
|
+
return { start, end: r[1] };
|
|
533
|
+
};
|
|
534
|
+
if (parts.length === 1 && parts[0] === 'default' && lang === 'ts') {
|
|
535
|
+
const m = /\bexport\s+default\b/.exec(code);
|
|
536
|
+
return m && { start: m.index, end: code.length };
|
|
537
|
+
}
|
|
538
|
+
if (parts.length === 1)
|
|
539
|
+
return hit(findIn(code, lang, parts[0], 0, code.length, null));
|
|
540
|
+
if (lang === 'go' && parts.length === 2) {
|
|
541
|
+
const r = findIn(code, lang, parts[1], 0, code.length, null, parts[0]);
|
|
542
|
+
if (r && /^func\s*\(/.test(code.slice(r[0])))
|
|
543
|
+
return hit(r);
|
|
544
|
+
const t = findIn(code, lang, parts[0], 0, code.length, null);
|
|
545
|
+
return t ? hit(findIn(code, lang, parts[1], t[0] + 1, t[1], 1)) : null;
|
|
546
|
+
}
|
|
547
|
+
if (lang === 'rs' && parts.length === 2) {
|
|
548
|
+
for (const [a, b] of rustImpls(code, parts[0])) {
|
|
549
|
+
const r = findIn(code, lang, parts[1], a + 1, b, 0);
|
|
550
|
+
if (r)
|
|
551
|
+
return hit(r);
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
let lo = 0;
|
|
556
|
+
let hi = code.length;
|
|
557
|
+
for (let k = 0; k < parts.length; k++) {
|
|
558
|
+
const r = findIn(code, lang, parts[k], lo, hi, k > 0 ? 1 : null);
|
|
559
|
+
if (!r)
|
|
560
|
+
return null;
|
|
561
|
+
if (k === parts.length - 1)
|
|
562
|
+
return hit(r);
|
|
563
|
+
lo = r[0] + 1;
|
|
564
|
+
hi = r[1];
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
export const isDefined = (file, symbol) => locate(file, symbol) != null;
|
package/dist/diff.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
// The spec diff: what a PR changed in a figure, by id. The SVG geometry follows from the spec, so the spec is the diff.
|
|
2
1
|
import { edgeId, isGroup, str, toBeat } from './model.js';
|
|
3
2
|
const show = (v) => (v == null ? '(none)' : `"${str(v)}"`);
|
|
4
|
-
/** The fields that changed, as `name "old" -> "new"` parts; empty when equal. */
|
|
5
3
|
const fields = (a, b, names) => names.filter((k) => str(a[k]) !== str(b[k])).map((k) => `${String(k)} ${show(a[k])} -> ${show(b[k])}`);
|
|
6
|
-
/** Compare two maps by key. The order is stable: changed first, then removed, then added. */
|
|
7
4
|
function compare(kind, before, after, fieldsOf) {
|
|
8
5
|
const out = [];
|
|
9
6
|
for (const [id, a] of before) {
|
|
@@ -28,7 +25,6 @@ const messages = (fig) => new Map((fig.steps ?? [])
|
|
|
28
25
|
export function diff(before, after) {
|
|
29
26
|
const boxes = (f) => {
|
|
30
27
|
const m = new Map();
|
|
31
|
-
// The group is its label, else its id; the top level has none.
|
|
32
28
|
const walk = (g, parent) => g.children.forEach((c) => (isGroup(c) ? walk(c, c.label != null ? str(c.label) : c.id) : m.set(c.id, { ...c, group: parent })));
|
|
33
29
|
walk(f.layout);
|
|
34
30
|
return m;
|
package/dist/draw.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
/** A file name from the question:
|
|
1
|
+
/** A file name from the question: at most 60 characters. */
|
|
2
2
|
export declare function slug(question: string): string;
|
|
3
|
-
/** The claude argv after the binary. The system prompt carries the agent text, the output path and the folder rule. */
|
|
4
3
|
export declare function agentArgs(o: {
|
|
5
4
|
question: string;
|
|
6
5
|
out: string;
|
|
@@ -8,5 +7,4 @@ export declare function agentArgs(o: {
|
|
|
8
7
|
maxTurns: number;
|
|
9
8
|
model?: string;
|
|
10
9
|
}): string[];
|
|
11
|
-
/** Run `flowfig draw`. Prints, and returns the exit code. */
|
|
12
10
|
export declare function runDraw(argv: string[]): Promise<number>;
|
package/dist/draw.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// `flowfig draw`: one command to the first figure. Claude Code reads the repo and renders; draw checks the result itself.
|
|
2
1
|
import { spawnSync } from 'node:child_process';
|
|
3
2
|
import { existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
|
|
4
3
|
import { dirname, resolve } from 'node:path';
|
|
@@ -10,7 +9,7 @@ import { verify } from './verify.js';
|
|
|
10
9
|
const ALLOWED = 'Read,Glob,Grep,Bash(npx flowfig *)';
|
|
11
10
|
const DISALLOWED = 'Edit,MultiEdit,NotebookEdit';
|
|
12
11
|
const USAGE = 'usage: flowfig draw "<question>" [--out <path>] [--model <alias>] [--max-turns <n>] [--json] [--open]';
|
|
13
|
-
/** A file name from the question:
|
|
12
|
+
/** A file name from the question: at most 60 characters. */
|
|
14
13
|
export function slug(question) {
|
|
15
14
|
const s = question
|
|
16
15
|
.toLowerCase()
|
|
@@ -21,7 +20,6 @@ export function slug(question) {
|
|
|
21
20
|
return s || 'figure';
|
|
22
21
|
}
|
|
23
22
|
const specPath = (out) => out.replace(/\.svg$/, '.json');
|
|
24
|
-
/** The claude argv after the binary. The system prompt carries the agent text, the output path and the folder rule. */
|
|
25
23
|
export function agentArgs(o) {
|
|
26
24
|
const spec = specPath(o.out);
|
|
27
25
|
const system = `${AGENT_TEXT.trimEnd()}\n\nWrite the spec to ${spec} with the Write tool. Then run exactly \`npx flowfig ${spec} ${o.out}\` as one command, alone on its line: no heredoc, no \`;\`, no \`&&\`, no pipe (the permission rule matches one plain command only). Then run \`npx flowfig verify ${o.out}\` the same way. Run every command from this folder, ${o.cwd}.`;
|
|
@@ -51,7 +49,6 @@ const fail = (message, code) => {
|
|
|
51
49
|
console.error(message);
|
|
52
50
|
return code;
|
|
53
51
|
};
|
|
54
|
-
/** Run `flowfig draw`. Prints, and returns the exit code. */
|
|
55
52
|
export async function runDraw(argv) {
|
|
56
53
|
const args = [...argv];
|
|
57
54
|
const flag = (n) => args.includes(n) && args.splice(args.indexOf(n), 1).length > 0;
|
|
@@ -103,7 +100,7 @@ export async function runDraw(argv) {
|
|
|
103
100
|
catch (e) {
|
|
104
101
|
return fail(`draw: ${e.message}`, 2);
|
|
105
102
|
}
|
|
106
|
-
//
|
|
103
|
+
// Compare the mtime: a figure from an earlier run must not pass.
|
|
107
104
|
const mtime = () => (existsSync(full) ? statSync(full).mtimeMs : undefined);
|
|
108
105
|
const before = mtime();
|
|
109
106
|
const run = spawnSync(bin, agentArgs({ question, out, cwd, maxTurns, model }), {
|
|
@@ -112,7 +109,7 @@ export async function runDraw(argv) {
|
|
|
112
109
|
stdio: ['ignore', 'pipe', 'inherit'],
|
|
113
110
|
maxBuffer: 64 * 1024 * 1024,
|
|
114
111
|
});
|
|
115
|
-
// The JSON is the last line that starts with {
|
|
112
|
+
// The JSON is the last line that starts with {; a warning can come before.
|
|
116
113
|
const line = (run.stdout ?? '')
|
|
117
114
|
.split('\n')
|
|
118
115
|
.reverse()
|
|
@@ -177,7 +174,6 @@ export async function runDraw(argv) {
|
|
|
177
174
|
}
|
|
178
175
|
stop();
|
|
179
176
|
let code = errors || stopped !== undefined ? 1 : 0;
|
|
180
|
-
// The SVG is on disk also with findings, so the user can look at it.
|
|
181
177
|
if (open) {
|
|
182
178
|
try {
|
|
183
179
|
const line = openedLine((await openSvg(full))[0]);
|
package/dist/edges.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CodeFile, type Lang, type Read } from './code.ts';
|
|
2
|
+
export type EdgeResult = 'found' | 'not-found' | 'unsure' | 'not-checked';
|
|
3
|
+
export declare const EDGE_LANGS: ReadonlySet<Lang>;
|
|
4
|
+
export declare function edgeResult(root: string, caller: string, callee: string | undefined, via: string | undefined, read?: Read, cache?: Map<string, CodeFile | null>): {
|
|
5
|
+
result: EdgeResult;
|
|
6
|
+
reason: string;
|
|
7
|
+
};
|