@health-samurai/react-components 0.0.0-alpha.18 → 0.0.0-alpha.20
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/LICENSE +21 -0
- package/dist/bundle.css +51 -33
- package/dist/src/components/code-editor/fhir-autocomplete.d.ts +70 -0
- package/dist/src/components/code-editor/fhir-autocomplete.d.ts.map +1 -0
- package/dist/src/components/code-editor/fhir-autocomplete.js +1849 -0
- package/dist/src/components/code-editor/fhir-autocomplete.js.map +1 -0
- package/dist/src/components/code-editor/fhir-autocomplete.test.js +1099 -0
- package/dist/src/components/code-editor/fhir-autocomplete.test.js.map +1 -0
- package/dist/src/components/code-editor/http/index.d.ts +9 -1
- package/dist/src/components/code-editor/http/index.d.ts.map +1 -1
- package/dist/src/components/code-editor/http/index.js +423 -3
- package/dist/src/components/code-editor/http/index.js.map +1 -1
- package/dist/src/components/code-editor/index.d.ts +13 -4
- package/dist/src/components/code-editor/index.d.ts.map +1 -1
- package/dist/src/components/code-editor/index.js +505 -96
- package/dist/src/components/code-editor/index.js.map +1 -1
- package/dist/src/components/code-editor/json-ast.d.ts +46 -0
- package/dist/src/components/code-editor/json-ast.d.ts.map +1 -0
- package/dist/src/components/code-editor/json-ast.js +465 -0
- package/dist/src/components/code-editor/json-ast.js.map +1 -0
- package/dist/src/components/code-editor/json-ast.test.js +206 -0
- package/dist/src/components/code-editor/json-ast.test.js.map +1 -0
- package/dist/src/components/code-editor/sql-completion.d.ts +22 -0
- package/dist/src/components/code-editor/sql-completion.d.ts.map +1 -0
- package/dist/src/components/code-editor/sql-completion.js +895 -0
- package/dist/src/components/code-editor/sql-completion.js.map +1 -0
- package/dist/src/components/date-picker-input.d.ts +10 -0
- package/dist/src/components/date-picker-input.d.ts.map +1 -0
- package/dist/src/components/date-picker-input.js +90 -0
- package/dist/src/components/date-picker-input.js.map +1 -0
- package/dist/src/components/date-picker-input.stories.js +76 -0
- package/dist/src/components/date-picker-input.stories.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/shadcn/components/ui/alert-dialog.d.ts +1 -1
- package/dist/src/shadcn/components/ui/calendar.d.ts +1 -1
- package/dist/src/shadcn/components/ui/carousel.d.ts +1 -1
- package/dist/src/shadcn/components/ui/chart.d.ts +3 -3
- package/dist/src/shadcn/components/ui/chart.d.ts.map +1 -1
- package/dist/src/shadcn/components/ui/chart.js +1 -1
- package/dist/src/shadcn/components/ui/chart.js.map +1 -1
- package/dist/src/shadcn/components/ui/command.d.ts +1 -1
- package/dist/src/shadcn/components/ui/pagination.d.ts +1 -1
- package/dist/src/shadcn/components/ui/resizable.stories.js +2 -2
- package/dist/src/shadcn/components/ui/resizable.stories.js.map +1 -1
- package/dist/src/shadcn/components/ui/sidebar.d.ts +4 -4
- package/dist/src/shadcn/components/ui/tabs.d.ts +3 -1
- package/dist/src/shadcn/components/ui/tabs.d.ts.map +1 -1
- package/dist/src/shadcn/components/ui/tabs.js +129 -2
- package/dist/src/shadcn/components/ui/tabs.js.map +1 -1
- package/dist/src/shadcn/components/ui/tabs.stories.js +1 -1
- package/dist/src/shadcn/components/ui/tabs.stories.js.map +1 -1
- package/dist/src/shadcn/components/ui/toggle-group.d.ts +1 -1
- package/dist/src/typography.css +1 -1
- package/package.json +24 -19
- package/src/components/code-editor/fhir-autocomplete.test.ts +993 -0
- package/src/components/code-editor/fhir-autocomplete.ts +2321 -0
- package/src/components/code-editor/http/index.ts +339 -2
- package/src/components/code-editor/index.tsx +593 -102
- package/src/components/code-editor/json-ast.test.ts +230 -0
- package/src/components/code-editor/json-ast.ts +590 -0
- package/src/components/code-editor/sql-completion.ts +1105 -0
- package/src/components/date-picker-input.stories.tsx +79 -0
- package/src/components/date-picker-input.tsx +104 -0
- package/src/index.tsx +1 -0
- package/src/shadcn/components/ui/chart.tsx +6 -3
- package/src/shadcn/components/ui/resizable.stories.tsx +2 -2
- package/src/shadcn/components/ui/tabs.stories.tsx +1 -1
- package/src/shadcn/components/ui/tabs.tsx +160 -2
- package/src/typography.css +1 -1
- package/dist/src/components/code-editor/http/grammar/http.test.d.ts +0 -2
- package/dist/src/components/code-editor/http/grammar/http.test.d.ts.map +0 -1
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import type { syntaxTree } from "@codemirror/language";
|
|
2
|
+
import type { SyntaxNode } from "@lezer/common";
|
|
3
|
+
|
|
4
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface ScopeView {
|
|
7
|
+
getString(key: string): string | null;
|
|
8
|
+
getStringArray(parentKey: string, arrayKey: string): string[];
|
|
9
|
+
getKeys(): string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DocumentContext {
|
|
13
|
+
fullPath: string[];
|
|
14
|
+
pos: number;
|
|
15
|
+
doc: string;
|
|
16
|
+
cursorPosition:
|
|
17
|
+
| { kind: "property"; prefix: string }
|
|
18
|
+
| { kind: "value"; key: string; prefix: string }
|
|
19
|
+
| { kind: "array-item"; parentKey: string; prefix: string }
|
|
20
|
+
| { kind: "none" };
|
|
21
|
+
getScope(levelsUp: number): ScopeView;
|
|
22
|
+
isInsideArray(): boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PropertyInfo {
|
|
26
|
+
name: string;
|
|
27
|
+
path: string[];
|
|
28
|
+
resourceType: string;
|
|
29
|
+
from: number;
|
|
30
|
+
to: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EmptyStringInfo {
|
|
34
|
+
from: number;
|
|
35
|
+
to: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── HTTP mode helper ───────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const HTTP_METHOD_RE = /^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s/;
|
|
41
|
+
|
|
42
|
+
function detectJsonStart(doc: string): number {
|
|
43
|
+
const firstLine = doc.slice(0, doc.indexOf("\n") >>> 0).trimStart();
|
|
44
|
+
if (HTTP_METHOD_RE.test(firstLine)) {
|
|
45
|
+
const bodyStart = doc.indexOf("\n\n");
|
|
46
|
+
if (bodyStart === -1) return 0;
|
|
47
|
+
return bodyStart + 2;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── JSON path at cursor ────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function getJsonPathAtCursor(doc: string, pos: number): string[] {
|
|
55
|
+
const path: string[] = [];
|
|
56
|
+
const arrayKeyStack: string[] = [];
|
|
57
|
+
let inString = false;
|
|
58
|
+
let isEscaped = false;
|
|
59
|
+
let currentKey = "";
|
|
60
|
+
let collectingKey = false;
|
|
61
|
+
let lastKey = "";
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < pos; i++) {
|
|
64
|
+
const ch = doc[i];
|
|
65
|
+
|
|
66
|
+
if (isEscaped) {
|
|
67
|
+
if (collectingKey) currentKey += ch;
|
|
68
|
+
isEscaped = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === "\\") {
|
|
72
|
+
isEscaped = true;
|
|
73
|
+
if (collectingKey) currentKey += ch;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (ch === '"') {
|
|
77
|
+
if (!inString) {
|
|
78
|
+
inString = true;
|
|
79
|
+
collectingKey = true;
|
|
80
|
+
currentKey = "";
|
|
81
|
+
} else {
|
|
82
|
+
inString = false;
|
|
83
|
+
if (collectingKey) {
|
|
84
|
+
lastKey = currentKey;
|
|
85
|
+
collectingKey = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (inString) {
|
|
91
|
+
if (collectingKey) currentKey += ch;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (ch === "[") {
|
|
95
|
+
arrayKeyStack.push(lastKey);
|
|
96
|
+
lastKey = "";
|
|
97
|
+
} else if (ch === "]") {
|
|
98
|
+
arrayKeyStack.pop();
|
|
99
|
+
lastKey = "";
|
|
100
|
+
} else if (ch === "{") {
|
|
101
|
+
const key =
|
|
102
|
+
lastKey ||
|
|
103
|
+
(arrayKeyStack.length > 0
|
|
104
|
+
? (arrayKeyStack[arrayKeyStack.length - 1] ?? "")
|
|
105
|
+
: "");
|
|
106
|
+
if (key) path.push(key);
|
|
107
|
+
lastKey = "";
|
|
108
|
+
} else if (ch === "}") {
|
|
109
|
+
path.pop();
|
|
110
|
+
lastKey = "";
|
|
111
|
+
} else if (ch === ",") {
|
|
112
|
+
lastKey = "";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return path;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Cursor position detection ──────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function isJsonValuePosition(beforeCursor: string): string | null {
|
|
121
|
+
// Don't match if a comma follows the value (value is complete)
|
|
122
|
+
if (/,\s*$/.test(beforeCursor)) return null;
|
|
123
|
+
const match = beforeCursor.match(/"?(\w+)"?\s*:\s*"?([^"]*)?$/);
|
|
124
|
+
if (match) return match[1] ?? null;
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isJsonPropertyPosition(beforeCursor: string): boolean {
|
|
129
|
+
if (beforeCursor === "" || beforeCursor === '"') return true;
|
|
130
|
+
if (/^"?[\w]*$/.test(beforeCursor)) return true;
|
|
131
|
+
if (/[{,]\s*"?[\w]*$/.test(beforeCursor)) return true;
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isInsideJsonArray(doc: string, pos: number): boolean {
|
|
136
|
+
let depth = 0;
|
|
137
|
+
let inStr = false;
|
|
138
|
+
let escaped = false;
|
|
139
|
+
for (let i = pos - 1; i >= 0; i--) {
|
|
140
|
+
const ch = doc[i];
|
|
141
|
+
if (escaped) {
|
|
142
|
+
escaped = false;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (ch === "\\") {
|
|
146
|
+
escaped = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (ch === '"') {
|
|
150
|
+
inStr = !inStr;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (inStr) continue;
|
|
154
|
+
if (ch === "}" || ch === "]") {
|
|
155
|
+
depth++;
|
|
156
|
+
} else if (ch === "{") {
|
|
157
|
+
if (depth === 0) return false;
|
|
158
|
+
depth--;
|
|
159
|
+
} else if (ch === "[") {
|
|
160
|
+
if (depth === 0) return true;
|
|
161
|
+
depth--;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Array-item detection ───────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function detectArrayItemContext(
|
|
170
|
+
doc: string,
|
|
171
|
+
pos: number,
|
|
172
|
+
): { parentKey: string; prefix: string } | null {
|
|
173
|
+
const textBefore = doc.slice(0, pos);
|
|
174
|
+
const arrayMatch = textBefore.match(
|
|
175
|
+
/"(\w+)"\s*:\s*\[\s*(?:"[^"]*"\s*,\s*)*"?([^"]*)$/s,
|
|
176
|
+
);
|
|
177
|
+
if (!arrayMatch) return null;
|
|
178
|
+
// If there are unmatched { after [, cursor is inside a nested object, not directly in array
|
|
179
|
+
const afterBracket = arrayMatch[2] ?? "";
|
|
180
|
+
let braceDepth = 0;
|
|
181
|
+
for (const ch of afterBracket) {
|
|
182
|
+
if (ch === "{") braceDepth++;
|
|
183
|
+
else if (ch === "}") braceDepth--;
|
|
184
|
+
}
|
|
185
|
+
if (braceDepth > 0) return null;
|
|
186
|
+
return { parentKey: arrayMatch[1]!, prefix: afterBracket };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Scope view (find values in ancestor objects) ───────────────────────
|
|
190
|
+
|
|
191
|
+
function findStringValueInObject(
|
|
192
|
+
doc: string,
|
|
193
|
+
objStart: number,
|
|
194
|
+
limit: number,
|
|
195
|
+
targetKey: string,
|
|
196
|
+
): string | null {
|
|
197
|
+
let fd = 0;
|
|
198
|
+
let fs = false;
|
|
199
|
+
let fe = false;
|
|
200
|
+
let lastKey = "";
|
|
201
|
+
let collecting = false;
|
|
202
|
+
let current = "";
|
|
203
|
+
let afterColon = false;
|
|
204
|
+
|
|
205
|
+
for (let i = objStart + 1; i < limit; i++) {
|
|
206
|
+
const ch = doc[i];
|
|
207
|
+
if (fe) {
|
|
208
|
+
if (collecting) current += ch;
|
|
209
|
+
fe = false;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (ch === "\\") {
|
|
213
|
+
fe = true;
|
|
214
|
+
if (collecting) current += ch;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (ch === '"') {
|
|
218
|
+
if (!fs) {
|
|
219
|
+
fs = true;
|
|
220
|
+
if (fd === 0) {
|
|
221
|
+
collecting = true;
|
|
222
|
+
current = "";
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
fs = false;
|
|
226
|
+
if (collecting) {
|
|
227
|
+
if (afterColon) {
|
|
228
|
+
if (lastKey === targetKey) return current;
|
|
229
|
+
afterColon = false;
|
|
230
|
+
} else {
|
|
231
|
+
lastKey = current;
|
|
232
|
+
}
|
|
233
|
+
collecting = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (fs) {
|
|
239
|
+
if (collecting) current += ch;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (ch === "{" || ch === "[") fd++;
|
|
243
|
+
else if (ch === "}" || ch === "]") fd--;
|
|
244
|
+
else if (ch === ":" && fd === 0) afterColon = true;
|
|
245
|
+
else if (ch === "," && fd === 0) {
|
|
246
|
+
afterColon = false;
|
|
247
|
+
lastKey = "";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function findStringArrayInObject(
|
|
254
|
+
doc: string,
|
|
255
|
+
objStart: number,
|
|
256
|
+
limit: number,
|
|
257
|
+
parentKey: string,
|
|
258
|
+
arrayKey: string,
|
|
259
|
+
): string[] {
|
|
260
|
+
// Find "parentKey": { ... "arrayKey": ["v1", "v2"] ... }
|
|
261
|
+
// or if parentKey is empty, find "arrayKey": [...] at top level
|
|
262
|
+
const searchDoc = doc.slice(objStart, limit);
|
|
263
|
+
let pattern: RegExp;
|
|
264
|
+
if (parentKey) {
|
|
265
|
+
pattern = new RegExp(
|
|
266
|
+
`"${parentKey}"\\s*:\\s*\\{[\\s\\S]*?"${arrayKey}"\\s*:\\s*\\[([\\s\\S]*?)\\]`,
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
pattern = new RegExp(`"${arrayKey}"\\s*:\\s*\\[([\\s\\S]*?)\\]`);
|
|
270
|
+
}
|
|
271
|
+
const match = searchDoc.match(pattern);
|
|
272
|
+
if (!match?.[1]) return [];
|
|
273
|
+
const urls: string[] = [];
|
|
274
|
+
const re = /"([^"]+)"/g;
|
|
275
|
+
let m: RegExpExecArray | null;
|
|
276
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
|
|
277
|
+
while ((m = re.exec(match[1])) !== null) {
|
|
278
|
+
if (m[1]) urls.push(m[1]);
|
|
279
|
+
}
|
|
280
|
+
return urls;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function findKeysInObject(
|
|
284
|
+
doc: string,
|
|
285
|
+
objStart: number,
|
|
286
|
+
limit: number,
|
|
287
|
+
): string[] {
|
|
288
|
+
const keys: string[] = [];
|
|
289
|
+
let fd = 0;
|
|
290
|
+
let fs = false;
|
|
291
|
+
let fe = false;
|
|
292
|
+
let collecting = false;
|
|
293
|
+
let current = "";
|
|
294
|
+
let afterColon = false;
|
|
295
|
+
|
|
296
|
+
for (let i = objStart + 1; i < limit; i++) {
|
|
297
|
+
const ch = doc[i];
|
|
298
|
+
if (fe) {
|
|
299
|
+
if (collecting) current += ch;
|
|
300
|
+
fe = false;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (ch === "\\") {
|
|
304
|
+
fe = true;
|
|
305
|
+
if (collecting) current += ch;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (ch === '"') {
|
|
309
|
+
if (!fs) {
|
|
310
|
+
fs = true;
|
|
311
|
+
if (fd === 0) {
|
|
312
|
+
collecting = true;
|
|
313
|
+
current = "";
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
fs = false;
|
|
317
|
+
if (collecting) {
|
|
318
|
+
if (!afterColon) {
|
|
319
|
+
keys.push(current);
|
|
320
|
+
}
|
|
321
|
+
collecting = false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (fs) {
|
|
327
|
+
if (collecting) current += ch;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (ch === "{" || ch === "[") fd++;
|
|
331
|
+
else if (ch === "}" || ch === "]") fd--;
|
|
332
|
+
else if (ch === ":" && fd === 0) afterColon = true;
|
|
333
|
+
else if (ch === "," && fd === 0) {
|
|
334
|
+
afterColon = false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return keys;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function buildScopeView(doc: string, pos: number, levelsUp: number): ScopeView {
|
|
341
|
+
// Forward scan to find enclosing objects — avoids string-tracking bugs
|
|
342
|
+
// from backward scanning when cursor is inside an unclosed string.
|
|
343
|
+
const objectStack: number[] = [];
|
|
344
|
+
let inString = false;
|
|
345
|
+
let isEscaped = false;
|
|
346
|
+
|
|
347
|
+
for (let i = 0; i < pos; i++) {
|
|
348
|
+
const ch = doc[i];
|
|
349
|
+
if (isEscaped) {
|
|
350
|
+
isEscaped = false;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (ch === "\\") {
|
|
354
|
+
isEscaped = true;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (ch === '"') {
|
|
358
|
+
inString = !inString;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (inString) continue;
|
|
362
|
+
if (ch === "{") {
|
|
363
|
+
objectStack.push(i);
|
|
364
|
+
} else if (ch === "}") {
|
|
365
|
+
objectStack.pop();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// objectStack[last] is innermost, objectStack[last - levelsUp] is target
|
|
370
|
+
const targetIdx = objectStack.length - 1 - levelsUp;
|
|
371
|
+
if (targetIdx < 0) {
|
|
372
|
+
return {
|
|
373
|
+
getString() {
|
|
374
|
+
return null;
|
|
375
|
+
},
|
|
376
|
+
getStringArray() {
|
|
377
|
+
return [];
|
|
378
|
+
},
|
|
379
|
+
getKeys() {
|
|
380
|
+
return [];
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const objStart = objectStack[targetIdx]!;
|
|
386
|
+
const scopeEnd = doc.length;
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
getString(key: string): string | null {
|
|
390
|
+
return findStringValueInObject(doc, objStart, scopeEnd, key);
|
|
391
|
+
},
|
|
392
|
+
getStringArray(parentKey: string, arrayKey: string): string[] {
|
|
393
|
+
return findStringArrayInObject(
|
|
394
|
+
doc,
|
|
395
|
+
objStart,
|
|
396
|
+
scopeEnd,
|
|
397
|
+
parentKey,
|
|
398
|
+
arrayKey,
|
|
399
|
+
);
|
|
400
|
+
},
|
|
401
|
+
getKeys(): string[] {
|
|
402
|
+
return findKeysInObject(doc, objStart, scopeEnd);
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── buildJsonDocumentContext ────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
export function buildJsonDocumentContext(
|
|
410
|
+
doc: string,
|
|
411
|
+
pos: number,
|
|
412
|
+
): DocumentContext {
|
|
413
|
+
const jsonStart = detectJsonStart(doc);
|
|
414
|
+
const jsonBody = doc.slice(jsonStart);
|
|
415
|
+
const posInBody = pos - jsonStart;
|
|
416
|
+
|
|
417
|
+
const fullPath = getJsonPathAtCursor(jsonBody, posInBody);
|
|
418
|
+
|
|
419
|
+
// Determine cursor position kind
|
|
420
|
+
const lineStart = doc.lastIndexOf("\n", pos - 1) + 1;
|
|
421
|
+
const beforeCursor = doc.slice(lineStart, pos).trimStart();
|
|
422
|
+
|
|
423
|
+
let cursorPosition: DocumentContext["cursorPosition"];
|
|
424
|
+
|
|
425
|
+
const valueKey = isJsonValuePosition(beforeCursor);
|
|
426
|
+
if (valueKey) {
|
|
427
|
+
cursorPosition = { kind: "value", key: valueKey, prefix: "" };
|
|
428
|
+
const wordMatch = beforeCursor.match(/"?(\w+)"?\s*:\s*"?([^"]*)?$/);
|
|
429
|
+
if (wordMatch?.[2] != null) {
|
|
430
|
+
cursorPosition.prefix = wordMatch[2];
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
const arrayItem = detectArrayItemContext(doc.slice(jsonStart), posInBody);
|
|
434
|
+
if (arrayItem) {
|
|
435
|
+
cursorPosition = {
|
|
436
|
+
kind: "array-item",
|
|
437
|
+
parentKey: arrayItem.parentKey,
|
|
438
|
+
prefix: arrayItem.prefix,
|
|
439
|
+
};
|
|
440
|
+
} else if (isJsonPropertyPosition(beforeCursor)) {
|
|
441
|
+
const wordMatch = beforeCursor.match(/"?(\w*)$/);
|
|
442
|
+
cursorPosition = { kind: "property", prefix: wordMatch?.[1] ?? "" };
|
|
443
|
+
} else {
|
|
444
|
+
cursorPosition = { kind: "none" };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
fullPath,
|
|
450
|
+
pos,
|
|
451
|
+
doc,
|
|
452
|
+
cursorPosition,
|
|
453
|
+
getScope(levelsUp: number): ScopeView {
|
|
454
|
+
return buildScopeView(jsonBody, posInBody, levelsUp);
|
|
455
|
+
},
|
|
456
|
+
isInsideArray(): boolean {
|
|
457
|
+
return isInsideJsonArray(jsonBody, posInBody);
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Validation helpers ─────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
export function walkJsonProperties(
|
|
465
|
+
doc: string,
|
|
466
|
+
tree: ReturnType<typeof syntaxTree>,
|
|
467
|
+
resourceTypeHint: string | null,
|
|
468
|
+
): { properties: PropertyInfo[]; emptyStrings: EmptyStringInfo[] } {
|
|
469
|
+
const properties: PropertyInfo[] = [];
|
|
470
|
+
const emptyStrings: EmptyStringInfo[] = [];
|
|
471
|
+
|
|
472
|
+
const rootObj = findRootJsonObject(doc, tree);
|
|
473
|
+
if (rootObj) {
|
|
474
|
+
walkJsonObject(
|
|
475
|
+
rootObj,
|
|
476
|
+
[],
|
|
477
|
+
resourceTypeHint,
|
|
478
|
+
doc,
|
|
479
|
+
properties,
|
|
480
|
+
emptyStrings,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { properties, emptyStrings };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function findRootJsonObject(
|
|
488
|
+
doc: string,
|
|
489
|
+
tree: ReturnType<typeof syntaxTree>,
|
|
490
|
+
): SyntaxNode | null {
|
|
491
|
+
const direct = tree.topNode.getChild("Object");
|
|
492
|
+
if (direct) return direct;
|
|
493
|
+
|
|
494
|
+
const bodyStart = doc.indexOf("\n\n");
|
|
495
|
+
if (bodyStart === -1) return null;
|
|
496
|
+
|
|
497
|
+
const jsonStart = bodyStart + 2;
|
|
498
|
+
if (jsonStart >= doc.length) return null;
|
|
499
|
+
|
|
500
|
+
const innerNode = tree.resolveInner(jsonStart, 1);
|
|
501
|
+
if (!innerNode) return null;
|
|
502
|
+
|
|
503
|
+
let node: SyntaxNode | null = innerNode;
|
|
504
|
+
while (node) {
|
|
505
|
+
if (node.name === "Object") return node;
|
|
506
|
+
if (node.name === "JsonText") {
|
|
507
|
+
return node.getChild("Object");
|
|
508
|
+
}
|
|
509
|
+
node = node.parent;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function walkJsonObject(
|
|
516
|
+
node: SyntaxNode,
|
|
517
|
+
parentPath: string[],
|
|
518
|
+
parentResourceType: string | null,
|
|
519
|
+
doc: string,
|
|
520
|
+
result: PropertyInfo[],
|
|
521
|
+
emptyStrings?: EmptyStringInfo[],
|
|
522
|
+
): void {
|
|
523
|
+
let ownResourceType: string | null = null;
|
|
524
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
525
|
+
if (child.name !== "Property") continue;
|
|
526
|
+
const nameNode = child.getChild("PropertyName");
|
|
527
|
+
if (!nameNode) continue;
|
|
528
|
+
const keyName = doc.slice(nameNode.from, nameNode.to).replace(/^"|"$/g, "");
|
|
529
|
+
if (keyName === "resourceType") {
|
|
530
|
+
for (let v = child.firstChild; v; v = v.nextSibling) {
|
|
531
|
+
if (v.name === "String") {
|
|
532
|
+
ownResourceType = doc.slice(v.from, v.to).replace(/^"|"$/g, "");
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const resourceType = ownResourceType ?? parentResourceType;
|
|
541
|
+
const path = ownResourceType ? [] : parentPath;
|
|
542
|
+
|
|
543
|
+
if (!resourceType) return;
|
|
544
|
+
|
|
545
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
546
|
+
if (child.name !== "Property") continue;
|
|
547
|
+
const nameNode = child.getChild("PropertyName");
|
|
548
|
+
if (!nameNode) continue;
|
|
549
|
+
const name = doc.slice(nameNode.from, nameNode.to).replace(/^"|"$/g, "");
|
|
550
|
+
|
|
551
|
+
result.push({
|
|
552
|
+
name,
|
|
553
|
+
path: [...path],
|
|
554
|
+
resourceType,
|
|
555
|
+
from: nameNode.from,
|
|
556
|
+
to: nameNode.to,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
for (let v = child.firstChild; v; v = v.nextSibling) {
|
|
560
|
+
if (v.name === "Object") {
|
|
561
|
+
walkJsonObject(
|
|
562
|
+
v,
|
|
563
|
+
[...path, name],
|
|
564
|
+
resourceType,
|
|
565
|
+
doc,
|
|
566
|
+
result,
|
|
567
|
+
emptyStrings,
|
|
568
|
+
);
|
|
569
|
+
} else if (v.name === "Array") {
|
|
570
|
+
for (let item = v.firstChild; item; item = item.nextSibling) {
|
|
571
|
+
if (item.name === "Object") {
|
|
572
|
+
walkJsonObject(
|
|
573
|
+
item,
|
|
574
|
+
[...path, name],
|
|
575
|
+
resourceType,
|
|
576
|
+
doc,
|
|
577
|
+
result,
|
|
578
|
+
emptyStrings,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} else if (v.name === "String" && emptyStrings) {
|
|
583
|
+
const raw = doc.slice(v.from, v.to);
|
|
584
|
+
if (raw === '""') {
|
|
585
|
+
emptyStrings.push({ from: v.from, to: v.to });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|