@getjack/jack 0.1.24 → 0.1.26
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/package.json +1 -1
- package/src/commands/services.ts +156 -36
- package/src/commands/update.ts +0 -1
- package/src/lib/control-plane.ts +86 -0
- package/src/lib/services/db-execute.ts +100 -1
- package/src/lib/services/db-list.ts +34 -7
- package/src/lib/services/sql-classifier.test.ts +18 -18
- package/src/lib/services/sql-classifier.ts +7 -7
- package/src/lib/services/vectorize-config.ts +569 -0
- package/src/lib/services/vectorize-create.ts +166 -0
- package/src/lib/services/vectorize-delete.ts +54 -0
- package/src/lib/services/vectorize-info.ts +52 -0
- package/src/lib/services/vectorize-list.ts +56 -0
- package/src/mcp/tools/index.ts +282 -0
- package/templates/AI-BINDINGS.md +181 -0
- package/templates/CLAUDE.md +30 -0
- package/templates/ai-chat/.jack.json +3 -4
- package/templates/ai-chat/src/index.ts +45 -5
- package/templates/ai-chat/src/jack-ai.ts +96 -0
- package/templates/semantic-search/.jack.json +3 -4
- package/templates/semantic-search/src/index.ts +70 -12
- package/templates/semantic-search/src/jack-ai.ts +96 -0
- package/templates/semantic-search/src/jack-vectorize.ts +165 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for reading and modifying Vectorize bindings in wrangler.jsonc
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { parseJsonc } from "../jsonc.ts";
|
|
7
|
+
|
|
8
|
+
export interface VectorizeBindingConfig {
|
|
9
|
+
binding: string; // e.g., "VECTORS" or "SEARCH_INDEX"
|
|
10
|
+
index_name: string; // e.g., "my-app-vectors"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface WranglerConfig {
|
|
14
|
+
vectorize?: Array<{
|
|
15
|
+
binding: string;
|
|
16
|
+
index_name?: string;
|
|
17
|
+
}>;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get existing Vectorize bindings from wrangler.jsonc
|
|
23
|
+
*/
|
|
24
|
+
export async function getExistingVectorizeBindings(
|
|
25
|
+
configPath: string,
|
|
26
|
+
): Promise<VectorizeBindingConfig[]> {
|
|
27
|
+
if (!existsSync(configPath)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const content = await Bun.file(configPath).text();
|
|
32
|
+
const config = parseJsonc<WranglerConfig>(content);
|
|
33
|
+
|
|
34
|
+
if (!config.vectorize || !Array.isArray(config.vectorize)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return config.vectorize
|
|
39
|
+
.filter((v) => v.binding && v.index_name)
|
|
40
|
+
.map((v) => ({
|
|
41
|
+
binding: v.binding,
|
|
42
|
+
index_name: v.index_name as string,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Add a Vectorize binding to wrangler.jsonc while preserving comments.
|
|
48
|
+
*/
|
|
49
|
+
export async function addVectorizeBinding(
|
|
50
|
+
configPath: string,
|
|
51
|
+
binding: VectorizeBindingConfig,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
if (!existsSync(configPath)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`wrangler.jsonc not found at ${configPath}. Create a wrangler.jsonc file first or run 'jack new' to create a new project.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const content = await Bun.file(configPath).text();
|
|
60
|
+
const config = parseJsonc<WranglerConfig>(content);
|
|
61
|
+
|
|
62
|
+
// Format the new binding entry
|
|
63
|
+
const bindingJson = formatVectorizeBindingEntry(binding);
|
|
64
|
+
|
|
65
|
+
let newContent: string;
|
|
66
|
+
|
|
67
|
+
if (config.vectorize && Array.isArray(config.vectorize)) {
|
|
68
|
+
// vectorize exists - append to the array
|
|
69
|
+
newContent = appendToVectorizeArray(content, bindingJson);
|
|
70
|
+
} else {
|
|
71
|
+
// vectorize doesn't exist - add it before closing brace
|
|
72
|
+
newContent = addVectorizeSection(content, bindingJson);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await Bun.write(configPath, newContent);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Format a Vectorize binding as a JSON object string with proper indentation
|
|
80
|
+
*/
|
|
81
|
+
function formatVectorizeBindingEntry(binding: VectorizeBindingConfig): string {
|
|
82
|
+
return `{
|
|
83
|
+
"binding": "${binding.binding}",
|
|
84
|
+
"index_name": "${binding.index_name}"
|
|
85
|
+
}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Append a new entry to an existing vectorize array.
|
|
90
|
+
*/
|
|
91
|
+
function appendToVectorizeArray(content: string, bindingJson: string): string {
|
|
92
|
+
// Find "vectorize" and then find its closing bracket
|
|
93
|
+
const vecMatch = content.match(/"vectorize"\s*:\s*\[/);
|
|
94
|
+
if (!vecMatch || vecMatch.index === undefined) {
|
|
95
|
+
throw new Error("Could not find vectorize array in config");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const arrayStartIndex = vecMatch.index + vecMatch[0].length;
|
|
99
|
+
const closingBracketIndex = findMatchingBracket(content, arrayStartIndex - 1, "[", "]");
|
|
100
|
+
if (closingBracketIndex === -1) {
|
|
101
|
+
throw new Error("Could not find closing bracket for vectorize array");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if array is empty or has content
|
|
105
|
+
const arrayContent = content.slice(arrayStartIndex, closingBracketIndex).trim();
|
|
106
|
+
const isEmpty = arrayContent === "" || isOnlyCommentsAndWhitespace(arrayContent);
|
|
107
|
+
|
|
108
|
+
let insertion: string;
|
|
109
|
+
if (isEmpty) {
|
|
110
|
+
insertion = `\n\t\t${bindingJson}\n\t`;
|
|
111
|
+
} else {
|
|
112
|
+
insertion = `,\n\t\t${bindingJson}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const beforeBracket = content.slice(0, closingBracketIndex);
|
|
116
|
+
const afterBracket = content.slice(closingBracketIndex);
|
|
117
|
+
|
|
118
|
+
if (isEmpty) {
|
|
119
|
+
return beforeBracket + insertion + afterBracket;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// For non-empty arrays, find the last closing brace of an object in the array
|
|
123
|
+
const lastObjectEnd = findLastObjectEndInArray(content, arrayStartIndex, closingBracketIndex);
|
|
124
|
+
if (lastObjectEnd === -1) {
|
|
125
|
+
return beforeBracket + insertion + afterBracket;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return content.slice(0, lastObjectEnd + 1) + insertion + content.slice(lastObjectEnd + 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Add a new vectorize section to the config.
|
|
133
|
+
*/
|
|
134
|
+
function addVectorizeSection(content: string, bindingJson: string): string {
|
|
135
|
+
const lastBraceIndex = content.lastIndexOf("}");
|
|
136
|
+
if (lastBraceIndex === -1) {
|
|
137
|
+
throw new Error("Invalid JSON: no closing brace found");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const beforeBrace = content.slice(0, lastBraceIndex);
|
|
141
|
+
const needsComma = shouldAddCommaBefore(beforeBrace);
|
|
142
|
+
|
|
143
|
+
const vecSection = `"vectorize": [
|
|
144
|
+
${bindingJson}
|
|
145
|
+
]`;
|
|
146
|
+
|
|
147
|
+
const trimmedBefore = beforeBrace.trimEnd();
|
|
148
|
+
|
|
149
|
+
let insertion: string;
|
|
150
|
+
if (needsComma) {
|
|
151
|
+
insertion = `,\n\t${vecSection}`;
|
|
152
|
+
} else {
|
|
153
|
+
insertion = `\n\t${vecSection}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return trimmedBefore + insertion + "\n" + content.slice(lastBraceIndex);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Remove a Vectorize binding from wrangler.jsonc by index_name.
|
|
161
|
+
*
|
|
162
|
+
* @returns true if binding was found and removed, false if not found
|
|
163
|
+
*/
|
|
164
|
+
export async function removeVectorizeBinding(
|
|
165
|
+
configPath: string,
|
|
166
|
+
indexName: string,
|
|
167
|
+
): Promise<boolean> {
|
|
168
|
+
if (!existsSync(configPath)) {
|
|
169
|
+
throw new Error(`wrangler.jsonc not found at ${configPath}. Cannot remove binding.`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const content = await Bun.file(configPath).text();
|
|
173
|
+
const config = parseJsonc<WranglerConfig>(content);
|
|
174
|
+
|
|
175
|
+
if (!config.vectorize || !Array.isArray(config.vectorize)) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const bindingIndex = config.vectorize.findIndex((v) => v.index_name === indexName);
|
|
180
|
+
if (bindingIndex === -1) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const newContent = removeVectorizeEntryFromContent(content, indexName);
|
|
185
|
+
if (newContent === content) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await Bun.write(configPath, newContent);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Remove a specific Vectorize entry from the vectorize array in content.
|
|
195
|
+
*/
|
|
196
|
+
function removeVectorizeEntryFromContent(content: string, indexName: string): string {
|
|
197
|
+
const vecMatch = content.match(/"vectorize"\s*:\s*\[/);
|
|
198
|
+
if (!vecMatch || vecMatch.index === undefined) {
|
|
199
|
+
return content;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const arrayStartIndex = vecMatch.index + vecMatch[0].length;
|
|
203
|
+
const closingBracketIndex = findMatchingBracket(content, arrayStartIndex - 1, "[", "]");
|
|
204
|
+
|
|
205
|
+
if (closingBracketIndex === -1) {
|
|
206
|
+
return content;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const arrayContent = content.slice(arrayStartIndex, closingBracketIndex);
|
|
210
|
+
|
|
211
|
+
// Find the object containing this index_name
|
|
212
|
+
const escapedName = indexName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
213
|
+
const indexNamePattern = new RegExp(`"index_name"\\s*:\\s*"${escapedName}"`);
|
|
214
|
+
|
|
215
|
+
const match = indexNamePattern.exec(arrayContent);
|
|
216
|
+
if (!match) {
|
|
217
|
+
return content;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Find the enclosing object boundaries
|
|
221
|
+
const matchPosInArray = match.index;
|
|
222
|
+
const objectStart = findObjectStartBefore(arrayContent, matchPosInArray);
|
|
223
|
+
const objectEnd = findObjectEndAfter(arrayContent, matchPosInArray);
|
|
224
|
+
|
|
225
|
+
if (objectStart === -1 || objectEnd === -1) {
|
|
226
|
+
return content;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let removeStart = objectStart;
|
|
230
|
+
let removeEnd = objectEnd + 1;
|
|
231
|
+
|
|
232
|
+
// Check for trailing comma after the object
|
|
233
|
+
const afterObject = arrayContent.slice(objectEnd + 1);
|
|
234
|
+
const trailingCommaMatch = afterObject.match(/^\s*,/);
|
|
235
|
+
|
|
236
|
+
// Check for leading comma before the object
|
|
237
|
+
const beforeObject = arrayContent.slice(0, objectStart);
|
|
238
|
+
const leadingCommaMatch = beforeObject.match(/,\s*$/);
|
|
239
|
+
|
|
240
|
+
if (trailingCommaMatch) {
|
|
241
|
+
removeEnd = objectEnd + 1 + trailingCommaMatch[0].length;
|
|
242
|
+
} else if (leadingCommaMatch) {
|
|
243
|
+
removeStart = objectStart - leadingCommaMatch[0].length;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const newArrayContent = arrayContent.slice(0, removeStart) + arrayContent.slice(removeEnd);
|
|
247
|
+
|
|
248
|
+
// Check if array is now effectively empty
|
|
249
|
+
const trimmedArray = newArrayContent.replace(/\/\/[^\n]*/g, "").trim();
|
|
250
|
+
if (trimmedArray === "" || trimmedArray === "[]") {
|
|
251
|
+
return removeVectorizeProperty(content, vecMatch.index, closingBracketIndex);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return content.slice(0, arrayStartIndex) + newArrayContent + content.slice(closingBracketIndex);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Remove the entire vectorize property when it becomes empty.
|
|
259
|
+
*/
|
|
260
|
+
function removeVectorizeProperty(
|
|
261
|
+
content: string,
|
|
262
|
+
propertyStart: number,
|
|
263
|
+
arrayEnd: number,
|
|
264
|
+
): string {
|
|
265
|
+
let removeStart = propertyStart;
|
|
266
|
+
let removeEnd = arrayEnd + 1;
|
|
267
|
+
|
|
268
|
+
const beforeProperty = content.slice(0, propertyStart);
|
|
269
|
+
const leadingCommaMatch = beforeProperty.match(/,\s*$/);
|
|
270
|
+
|
|
271
|
+
const afterProperty = content.slice(arrayEnd + 1);
|
|
272
|
+
const trailingCommaMatch = afterProperty.match(/^\s*,/);
|
|
273
|
+
|
|
274
|
+
if (leadingCommaMatch) {
|
|
275
|
+
removeStart = propertyStart - leadingCommaMatch[0].length;
|
|
276
|
+
} else if (trailingCommaMatch) {
|
|
277
|
+
removeEnd = arrayEnd + 1 + trailingCommaMatch[0].length;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return content.slice(0, removeStart) + content.slice(removeEnd);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Helper functions (same as in wrangler-config.ts)
|
|
284
|
+
|
|
285
|
+
function findMatchingBracket(
|
|
286
|
+
content: string,
|
|
287
|
+
startIndex: number,
|
|
288
|
+
openChar: string,
|
|
289
|
+
closeChar: string,
|
|
290
|
+
): number {
|
|
291
|
+
let depth = 0;
|
|
292
|
+
let inString = false;
|
|
293
|
+
let stringChar = "";
|
|
294
|
+
let escaped = false;
|
|
295
|
+
let inLineComment = false;
|
|
296
|
+
let inBlockComment = false;
|
|
297
|
+
|
|
298
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
299
|
+
const char = content[i] ?? "";
|
|
300
|
+
const next = content[i + 1] ?? "";
|
|
301
|
+
|
|
302
|
+
if (inLineComment) {
|
|
303
|
+
if (char === "\n") inLineComment = false;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (inBlockComment) {
|
|
308
|
+
if (char === "*" && next === "/") {
|
|
309
|
+
inBlockComment = false;
|
|
310
|
+
i++;
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (inString) {
|
|
316
|
+
if (escaped) {
|
|
317
|
+
escaped = false;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (char === "\\") {
|
|
321
|
+
escaped = true;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (char === stringChar) {
|
|
325
|
+
inString = false;
|
|
326
|
+
stringChar = "";
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (char === "/" && next === "/") {
|
|
332
|
+
inLineComment = true;
|
|
333
|
+
i++;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (char === "/" && next === "*") {
|
|
337
|
+
inBlockComment = true;
|
|
338
|
+
i++;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (char === '"' || char === "'") {
|
|
343
|
+
inString = true;
|
|
344
|
+
stringChar = char;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (char === openChar) {
|
|
349
|
+
depth++;
|
|
350
|
+
} else if (char === closeChar) {
|
|
351
|
+
depth--;
|
|
352
|
+
if (depth === 0) return i;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return -1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isOnlyCommentsAndWhitespace(content: string): boolean {
|
|
360
|
+
let inLineComment = false;
|
|
361
|
+
let inBlockComment = false;
|
|
362
|
+
|
|
363
|
+
for (let i = 0; i < content.length; i++) {
|
|
364
|
+
const char = content[i] ?? "";
|
|
365
|
+
const next = content[i + 1] ?? "";
|
|
366
|
+
|
|
367
|
+
if (inLineComment) {
|
|
368
|
+
if (char === "\n") inLineComment = false;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (inBlockComment) {
|
|
373
|
+
if (char === "*" && next === "/") {
|
|
374
|
+
inBlockComment = false;
|
|
375
|
+
i++;
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (char === "/" && next === "/") {
|
|
381
|
+
inLineComment = true;
|
|
382
|
+
i++;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (char === "/" && next === "*") {
|
|
387
|
+
inBlockComment = true;
|
|
388
|
+
i++;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!/\s/.test(char)) return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function findLastObjectEndInArray(content: string, startIndex: number, endIndex: number): number {
|
|
399
|
+
let lastBraceIndex = -1;
|
|
400
|
+
let inString = false;
|
|
401
|
+
let stringChar = "";
|
|
402
|
+
let escaped = false;
|
|
403
|
+
let inLineComment = false;
|
|
404
|
+
let inBlockComment = false;
|
|
405
|
+
|
|
406
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
407
|
+
const char = content[i] ?? "";
|
|
408
|
+
const next = content[i + 1] ?? "";
|
|
409
|
+
|
|
410
|
+
if (inLineComment) {
|
|
411
|
+
if (char === "\n") inLineComment = false;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (inBlockComment) {
|
|
416
|
+
if (char === "*" && next === "/") {
|
|
417
|
+
inBlockComment = false;
|
|
418
|
+
i++;
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (inString) {
|
|
424
|
+
if (escaped) {
|
|
425
|
+
escaped = false;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (char === "\\") {
|
|
429
|
+
escaped = true;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (char === stringChar) {
|
|
433
|
+
inString = false;
|
|
434
|
+
stringChar = "";
|
|
435
|
+
}
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (char === "/" && next === "/") {
|
|
440
|
+
inLineComment = true;
|
|
441
|
+
i++;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (char === "/" && next === "*") {
|
|
446
|
+
inBlockComment = true;
|
|
447
|
+
i++;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (char === '"' || char === "'") {
|
|
452
|
+
inString = true;
|
|
453
|
+
stringChar = char;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (char === "}") lastBraceIndex = i;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return lastBraceIndex;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function shouldAddCommaBefore(content: string): boolean {
|
|
464
|
+
let i = content.length - 1;
|
|
465
|
+
|
|
466
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
467
|
+
if (content[j] === "\n") {
|
|
468
|
+
const lineStart = content.lastIndexOf("\n", j - 1) + 1;
|
|
469
|
+
const line = content.slice(lineStart, j);
|
|
470
|
+
const commentIndex = findLineCommentStart(line);
|
|
471
|
+
if (commentIndex !== -1) {
|
|
472
|
+
i = lineStart + commentIndex - 1;
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
while (i >= 0 && /\s/.test(content[i] ?? "")) {
|
|
479
|
+
i--;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (i < 0) return false;
|
|
483
|
+
|
|
484
|
+
const lastChar = content[i];
|
|
485
|
+
return lastChar !== "{" && lastChar !== "[" && lastChar !== ",";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function findLineCommentStart(line: string): number {
|
|
489
|
+
let inString = false;
|
|
490
|
+
let stringChar = "";
|
|
491
|
+
let escaped = false;
|
|
492
|
+
|
|
493
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
494
|
+
const char = line[i] ?? "";
|
|
495
|
+
const next = line[i + 1] ?? "";
|
|
496
|
+
|
|
497
|
+
if (inString) {
|
|
498
|
+
if (escaped) {
|
|
499
|
+
escaped = false;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (char === "\\") {
|
|
503
|
+
escaped = true;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (char === stringChar) {
|
|
507
|
+
inString = false;
|
|
508
|
+
stringChar = "";
|
|
509
|
+
}
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (char === '"' || char === "'") {
|
|
514
|
+
inString = true;
|
|
515
|
+
stringChar = char;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (char === "/" && next === "/") return i;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return -1;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function findObjectStartBefore(content: string, fromPos: number): number {
|
|
526
|
+
let depth = 0;
|
|
527
|
+
for (let i = fromPos; i >= 0; i--) {
|
|
528
|
+
const char = content[i];
|
|
529
|
+
if (char === "}") depth++;
|
|
530
|
+
if (char === "{") {
|
|
531
|
+
if (depth === 0) return i;
|
|
532
|
+
depth--;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return -1;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function findObjectEndAfter(content: string, fromPos: number): number {
|
|
539
|
+
let depth = 0;
|
|
540
|
+
let inString = false;
|
|
541
|
+
let escaped = false;
|
|
542
|
+
|
|
543
|
+
for (let i = fromPos; i < content.length; i++) {
|
|
544
|
+
const char = content[i];
|
|
545
|
+
|
|
546
|
+
if (inString) {
|
|
547
|
+
if (escaped) {
|
|
548
|
+
escaped = false;
|
|
549
|
+
} else if (char === "\\") {
|
|
550
|
+
escaped = true;
|
|
551
|
+
} else if (char === '"') {
|
|
552
|
+
inString = false;
|
|
553
|
+
}
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (char === '"') {
|
|
558
|
+
inString = true;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (char === "{") depth++;
|
|
563
|
+
if (char === "}") {
|
|
564
|
+
if (depth === 0) return i;
|
|
565
|
+
depth--;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return -1;
|
|
569
|
+
}
|