@checkstack/ui 0.2.3 → 0.3.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/CHANGELOG.md +48 -0
- package/package.json +10 -3
- package/src/components/CodeEditor/CodeEditor.tsx +420 -0
- package/src/components/CodeEditor/index.ts +10 -0
- package/src/components/CodeEditor/languageSupport/enterBehavior.test.ts +173 -0
- package/src/components/CodeEditor/languageSupport/enterBehavior.ts +35 -0
- package/src/components/CodeEditor/languageSupport/index.ts +22 -0
- package/src/components/CodeEditor/languageSupport/json.test.ts +271 -0
- package/src/components/CodeEditor/languageSupport/json.ts +240 -0
- package/src/components/CodeEditor/languageSupport/markdown.test.ts +245 -0
- package/src/components/CodeEditor/languageSupport/markdown.ts +183 -0
- package/src/components/CodeEditor/languageSupport/types.ts +48 -0
- package/src/components/CodeEditor/languageSupport/xml.test.ts +236 -0
- package/src/components/CodeEditor/languageSupport/xml.ts +194 -0
- package/src/components/CodeEditor/languageSupport/yaml.test.ts +200 -0
- package/src/components/CodeEditor/languageSupport/yaml.ts +205 -0
- package/src/components/DynamicForm/DynamicForm.tsx +2 -24
- package/src/components/DynamicForm/DynamicOptionsField.tsx +48 -21
- package/src/components/DynamicForm/FormField.tsx +38 -70
- package/src/components/DynamicForm/JsonField.tsx +19 -25
- package/src/components/DynamicForm/KeyValueEditor.tsx +314 -0
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +465 -0
- package/src/components/DynamicForm/index.ts +13 -0
- package/src/components/DynamicForm/types.ts +14 -8
- package/src/components/DynamicForm/utils.test.ts +390 -0
- package/src/components/DynamicForm/utils.ts +142 -3
- package/src/components/StrategyConfigCard.tsx +8 -4
- package/src/index.ts +1 -1
- package/src/components/TemplateEditor.test.ts +0 -156
- package/src/components/TemplateEditor.tsx +0 -435
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for CodeEditor Enter key behavior.
|
|
3
|
+
* Extracted for testability.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if the cursor position is between matching brackets or tags.
|
|
8
|
+
* Used to determine if Enter should "split" the brackets/tags.
|
|
9
|
+
*
|
|
10
|
+
* @param textBefore - Text before the cursor position
|
|
11
|
+
* @param textAfter - Text after the cursor position
|
|
12
|
+
* @returns true if cursor is between matching brackets/tags
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Returns true for:
|
|
16
|
+
* // {|} - cursor between curly braces
|
|
17
|
+
* // [|] - cursor between square brackets
|
|
18
|
+
* // <tag>|</tag> - cursor between opening and closing tags
|
|
19
|
+
*/
|
|
20
|
+
export function isBetweenBrackets(
|
|
21
|
+
textBefore: string,
|
|
22
|
+
textAfter: string,
|
|
23
|
+
): boolean {
|
|
24
|
+
const charBefore = textBefore.slice(-1);
|
|
25
|
+
const charAfter = textAfter.charAt(0);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
// Curly braces: {|}
|
|
29
|
+
(charBefore === "{" && charAfter === "}") ||
|
|
30
|
+
// Square brackets: [|]
|
|
31
|
+
(charBefore === "[" && charAfter === "]") ||
|
|
32
|
+
// XML/HTML tags: <tag>|</tag>
|
|
33
|
+
(charBefore === ">" && charAfter === "<" && textAfter.startsWith("</"))
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type { LanguageSupport, DecorationRange } from "./types";
|
|
2
|
+
export {
|
|
3
|
+
jsonLanguageSupport,
|
|
4
|
+
isValidJsonTemplatePosition,
|
|
5
|
+
calculateJsonIndentation,
|
|
6
|
+
} from "./json";
|
|
7
|
+
export {
|
|
8
|
+
yamlLanguageSupport,
|
|
9
|
+
isValidYamlTemplatePosition,
|
|
10
|
+
calculateYamlIndentation,
|
|
11
|
+
} from "./yaml";
|
|
12
|
+
export {
|
|
13
|
+
xmlLanguageSupport,
|
|
14
|
+
isValidXmlTemplatePosition,
|
|
15
|
+
calculateXmlIndentation,
|
|
16
|
+
} from "./xml";
|
|
17
|
+
export {
|
|
18
|
+
markdownLanguageSupport,
|
|
19
|
+
isValidMarkdownTemplatePosition,
|
|
20
|
+
calculateMarkdownIndentation,
|
|
21
|
+
} from "./markdown";
|
|
22
|
+
export { isBetweenBrackets } from "./enterBehavior";
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { isValidJsonTemplatePosition, calculateJsonIndentation } from "./json";
|
|
4
|
+
|
|
5
|
+
describe("isValidJsonTemplatePosition", () => {
|
|
6
|
+
describe("string values (inside quotes)", () => {
|
|
7
|
+
it("returns true when inside a string value after colon", () => {
|
|
8
|
+
// Cursor at: {"name": "hello|
|
|
9
|
+
expect(isValidJsonTemplatePosition('{"name": "hello')).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns true when at start of string value", () => {
|
|
13
|
+
// Cursor at: {"name": "|
|
|
14
|
+
expect(isValidJsonTemplatePosition('{"name": "')).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns true for nested object string values", () => {
|
|
18
|
+
// Cursor at: {"user": {"name": "test|
|
|
19
|
+
expect(isValidJsonTemplatePosition('{"user": {"name": "test')).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns true for string with escaped quotes", () => {
|
|
23
|
+
// Cursor at: {"msg": "say \"hello|
|
|
24
|
+
expect(isValidJsonTemplatePosition('{"msg": "say \\"hello')).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns true when cursor is in middle of string value", () => {
|
|
28
|
+
// Cursor at: {"name": "hel|lo"
|
|
29
|
+
expect(isValidJsonTemplatePosition('{"name": "hel')).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("bare number value positions (outside quotes)", () => {
|
|
34
|
+
it("returns true for bare value position after colon", () => {
|
|
35
|
+
// Cursor at: {"count": |
|
|
36
|
+
// This is what the autocomplete will check (text BEFORE the {{ starts)
|
|
37
|
+
expect(isValidJsonTemplatePosition('{"count": ')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns true with auto-closed brackets (text before first {)", () => {
|
|
41
|
+
// When user types { in value position, editor auto-inserts }
|
|
42
|
+
// But the autocomplete checks text BEFORE the {{ pattern
|
|
43
|
+
// So it would check: {"count":
|
|
44
|
+
expect(isValidJsonTemplatePosition('{"count": ')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns false when {{ already typed (looks like objects to parser)", () => {
|
|
48
|
+
// This function itself returns false for {{ because it looks like objects
|
|
49
|
+
// BUT the autocomplete logic uses textBeforeTemplate which excludes {{
|
|
50
|
+
expect(isValidJsonTemplatePosition('{"count": {{')).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns true for nested object bare value", () => {
|
|
54
|
+
// Cursor at: {"data": {"count": |
|
|
55
|
+
expect(isValidJsonTemplatePosition('{"data": {"count": ')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("array element positions", () => {
|
|
60
|
+
it("returns true at start of array", () => {
|
|
61
|
+
// Cursor at: [|
|
|
62
|
+
expect(isValidJsonTemplatePosition("[")).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns true inside array string element", () => {
|
|
66
|
+
// Cursor at: ["hello|
|
|
67
|
+
expect(isValidJsonTemplatePosition('["hello')).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns true for bare array element position", () => {
|
|
71
|
+
// Cursor at: [|
|
|
72
|
+
expect(isValidJsonTemplatePosition("[ ")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns true after comma in array (value position)", () => {
|
|
76
|
+
// Cursor at: [1, |
|
|
77
|
+
expect(isValidJsonTemplatePosition("[1, ")).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns true in nested array", () => {
|
|
81
|
+
// Cursor at: [[|
|
|
82
|
+
expect(isValidJsonTemplatePosition("[[")).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns true for array in object value", () => {
|
|
86
|
+
// Cursor at: {"items": [|
|
|
87
|
+
expect(isValidJsonTemplatePosition('{"items": [')).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("invalid positions (keys and structural)", () => {
|
|
92
|
+
it("returns false at start of object (key position)", () => {
|
|
93
|
+
// Cursor at: {|
|
|
94
|
+
expect(isValidJsonTemplatePosition("{")).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns false for property key position", () => {
|
|
98
|
+
// Cursor at: {"|
|
|
99
|
+
expect(isValidJsonTemplatePosition('{"')).toBe(true); // Inside quotes counts
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns false after comma in object (key position)", () => {
|
|
103
|
+
// Cursor at: {"a": 1, |
|
|
104
|
+
expect(isValidJsonTemplatePosition('{"a": 1, ')).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns false after closing brace", () => {
|
|
108
|
+
// Cursor at: {"a": 1}|
|
|
109
|
+
expect(isValidJsonTemplatePosition('{"a": 1}')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns false after closing bracket", () => {
|
|
113
|
+
// Cursor at: [1, 2]|
|
|
114
|
+
expect(isValidJsonTemplatePosition("[1, 2]")).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns false for empty string", () => {
|
|
118
|
+
expect(isValidJsonTemplatePosition("")).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns false for just whitespace", () => {
|
|
122
|
+
expect(isValidJsonTemplatePosition(" ")).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("complex nested structures", () => {
|
|
127
|
+
it("handles deeply nested objects correctly", () => {
|
|
128
|
+
// Valid: inside string value in nested structure
|
|
129
|
+
expect(isValidJsonTemplatePosition('{"a": {"b": {"c": "test')).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("handles mixed arrays and objects", () => {
|
|
133
|
+
// Valid: inside array that's a value
|
|
134
|
+
expect(isValidJsonTemplatePosition('{"items": [{"name": "test')).toBe(
|
|
135
|
+
true,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("handles array of objects - key position after comma", () => {
|
|
140
|
+
// Invalid: after comma inside object (key position)
|
|
141
|
+
expect(isValidJsonTemplatePosition('[{"a": 1}, {"')).toBe(true); // Inside quotes
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("handles array of objects - after object close", () => {
|
|
145
|
+
// This is after the comma in array, which IS a value position
|
|
146
|
+
expect(isValidJsonTemplatePosition('[{"a": 1}, ')).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("edge cases", () => {
|
|
151
|
+
it("handles colon inside string correctly", () => {
|
|
152
|
+
// The colon is inside the string, not a separator
|
|
153
|
+
expect(isValidJsonTemplatePosition('{"url": "http:')).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("handles multiple colons", () => {
|
|
157
|
+
expect(isValidJsonTemplatePosition('{"a": 1, "b": ')).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("handles string value followed by comma correctly", () => {
|
|
161
|
+
// After the comma in an object, we're in key position
|
|
162
|
+
expect(isValidJsonTemplatePosition('{"a": "1", ')).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("handles incomplete structures", () => {
|
|
166
|
+
expect(isValidJsonTemplatePosition('{"a')).toBe(true); // Inside key string
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("calculateJsonIndentation", () => {
|
|
172
|
+
const INDENT_UNIT = 2;
|
|
173
|
+
|
|
174
|
+
describe("basic indentation", () => {
|
|
175
|
+
it("returns 0 for empty string", () => {
|
|
176
|
+
expect(calculateJsonIndentation("", INDENT_UNIT)).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("returns indentUnit after opening brace", () => {
|
|
180
|
+
expect(calculateJsonIndentation("{", INDENT_UNIT)).toBe(INDENT_UNIT);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("returns indentUnit after opening bracket", () => {
|
|
184
|
+
expect(calculateJsonIndentation("[", INDENT_UNIT)).toBe(INDENT_UNIT);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("returns 0 after closed object", () => {
|
|
188
|
+
expect(calculateJsonIndentation("{}", INDENT_UNIT)).toBe(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns 0 after closed array", () => {
|
|
192
|
+
expect(calculateJsonIndentation("[]", INDENT_UNIT)).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("nested structures", () => {
|
|
197
|
+
it("returns 2 * indentUnit for nested object", () => {
|
|
198
|
+
expect(calculateJsonIndentation('{"a": {', INDENT_UNIT)).toBe(
|
|
199
|
+
2 * INDENT_UNIT,
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("returns 2 * indentUnit for nested array", () => {
|
|
204
|
+
expect(calculateJsonIndentation('{"items": [', INDENT_UNIT)).toBe(
|
|
205
|
+
2 * INDENT_UNIT,
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("calculates correct depth for complex nesting", () => {
|
|
210
|
+
// {"a": {"b": [{"c":
|
|
211
|
+
expect(calculateJsonIndentation('{"a": {"b": [{"c": ', INDENT_UNIT)).toBe(
|
|
212
|
+
4 * INDENT_UNIT,
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("template handling (regression tests)", () => {
|
|
218
|
+
it("ignores {{ template opening braces", () => {
|
|
219
|
+
// Template {{ should NOT increase depth
|
|
220
|
+
expect(calculateJsonIndentation('{"foo": {{', INDENT_UNIT)).toBe(
|
|
221
|
+
INDENT_UNIT,
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("ignores }} template closing braces", () => {
|
|
226
|
+
// Template }} should NOT decrease depth
|
|
227
|
+
expect(
|
|
228
|
+
calculateJsonIndentation('{"foo": {{payload.title}}', INDENT_UNIT),
|
|
229
|
+
).toBe(INDENT_UNIT);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("maintains correct indentation after template value", () => {
|
|
233
|
+
// After a complete template, depth should still be 1 (inside the object)
|
|
234
|
+
expect(
|
|
235
|
+
calculateJsonIndentation('{"foo": {{payload.title}},\n', INDENT_UNIT),
|
|
236
|
+
).toBe(INDENT_UNIT);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("handles template followed by more properties", () => {
|
|
240
|
+
// Cursor after template value, ready for next property
|
|
241
|
+
const text = `{
|
|
242
|
+
"foo": "bar",
|
|
243
|
+
"bar": {{payload.title}},
|
|
244
|
+
`;
|
|
245
|
+
expect(calculateJsonIndentation(text, INDENT_UNIT)).toBe(INDENT_UNIT);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("handles multiple templates in same object", () => {
|
|
249
|
+
const text = `{
|
|
250
|
+
"a": {{payload.a}},
|
|
251
|
+
"b": {{payload.b}},
|
|
252
|
+
`;
|
|
253
|
+
expect(calculateJsonIndentation(text, INDENT_UNIT)).toBe(INDENT_UNIT);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("string handling", () => {
|
|
258
|
+
it("ignores braces inside strings", () => {
|
|
259
|
+
// Braces inside strings should not affect depth
|
|
260
|
+
expect(calculateJsonIndentation('{"msg": "hello {"', INDENT_UNIT)).toBe(
|
|
261
|
+
INDENT_UNIT,
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("ignores template-like patterns inside strings", () => {
|
|
266
|
+
expect(
|
|
267
|
+
calculateJsonIndentation('{"msg": "use {{var}}"', INDENT_UNIT),
|
|
268
|
+
).toBe(INDENT_UNIT);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { json } from "@codemirror/lang-json";
|
|
2
|
+
import { Decoration } from "@codemirror/view";
|
|
3
|
+
import type { LanguageSupport, DecorationRange } from "./types";
|
|
4
|
+
|
|
5
|
+
// Decoration marks for JSON syntax highlighting using inline styles
|
|
6
|
+
// (higher specificity than Lezer parser's CSS classes)
|
|
7
|
+
const jsonStringMark = Decoration.mark({
|
|
8
|
+
attributes: { style: "color: hsl(142.1, 76.2%, 36.3%)" },
|
|
9
|
+
});
|
|
10
|
+
const jsonPropertyMark = Decoration.mark({
|
|
11
|
+
attributes: { style: "color: hsl(280, 65%, 60%)" },
|
|
12
|
+
});
|
|
13
|
+
const jsonNumberMark = Decoration.mark({
|
|
14
|
+
attributes: { style: "color: hsl(217.2, 91.2%, 59.8%)" },
|
|
15
|
+
});
|
|
16
|
+
const jsonKeywordMark = Decoration.mark({
|
|
17
|
+
attributes: { style: "color: hsl(280, 65%, 60%)" },
|
|
18
|
+
});
|
|
19
|
+
const templateMark = Decoration.mark({
|
|
20
|
+
attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if cursor is in a valid JSON template position.
|
|
25
|
+
* Returns true if:
|
|
26
|
+
* 1. Inside a string value (for string templates like "{{payload.name}}")
|
|
27
|
+
* 2. In a value position after a colon but outside quotes (for number templates like {{payload.count}})
|
|
28
|
+
* 3. Inside an array element position
|
|
29
|
+
*
|
|
30
|
+
* @internal Exported for testing
|
|
31
|
+
*/
|
|
32
|
+
export function isValidJsonTemplatePosition(text: string): boolean {
|
|
33
|
+
let insideString = false;
|
|
34
|
+
let inValuePosition = false;
|
|
35
|
+
// Stack to track object/array nesting: 'o' for object, 'a' for array
|
|
36
|
+
const nestingStack: ("o" | "a")[] = [];
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < text.length; i++) {
|
|
39
|
+
const char = text[i];
|
|
40
|
+
const prevChar = i > 0 ? text[i - 1] : "";
|
|
41
|
+
|
|
42
|
+
if (char === '"' && prevChar !== "\\") {
|
|
43
|
+
insideString = !insideString;
|
|
44
|
+
} else if (!insideString) {
|
|
45
|
+
switch (char) {
|
|
46
|
+
case ":": {
|
|
47
|
+
inValuePosition = true;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case ",": {
|
|
51
|
+
// After comma: in arrays we stay in value position, in objects we go to key position
|
|
52
|
+
const currentContext = nestingStack.at(-1);
|
|
53
|
+
inValuePosition = currentContext === "a";
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case "{": {
|
|
57
|
+
// Start of object - next is a key position, not a value
|
|
58
|
+
nestingStack.push("o");
|
|
59
|
+
inValuePosition = false;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "[": {
|
|
63
|
+
// Start of array - next is a value position
|
|
64
|
+
nestingStack.push("a");
|
|
65
|
+
inValuePosition = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "}": {
|
|
69
|
+
nestingStack.pop();
|
|
70
|
+
inValuePosition = false;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "]": {
|
|
74
|
+
nestingStack.pop();
|
|
75
|
+
inValuePosition = false;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Valid if inside a string OR in a value position (for bare number templates)
|
|
83
|
+
return insideString || inValuePosition;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build comprehensive JSON + template decorations.
|
|
88
|
+
* This uses regex-based matching to apply consistent syntax highlighting
|
|
89
|
+
* that won't be confused by template expressions.
|
|
90
|
+
*/
|
|
91
|
+
function buildJsonDecorations(doc: string): DecorationRange[] {
|
|
92
|
+
const ranges: DecorationRange[] = [];
|
|
93
|
+
|
|
94
|
+
// Match templates first (highest priority)
|
|
95
|
+
const templateRegex = /\{\{[\w.[\]]*\}\}/g;
|
|
96
|
+
let match;
|
|
97
|
+
while ((match = templateRegex.exec(doc)) !== null) {
|
|
98
|
+
ranges.push({
|
|
99
|
+
from: match.index,
|
|
100
|
+
to: match.index + match[0].length,
|
|
101
|
+
decoration: templateMark,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Match property names: "key":
|
|
106
|
+
const propertyRegex = /"([^"\\]|\\.)*"\s*:/g;
|
|
107
|
+
while ((match = propertyRegex.exec(doc)) !== null) {
|
|
108
|
+
// Only highlight the string part, not the colon
|
|
109
|
+
const colonPos = match[0].lastIndexOf(":");
|
|
110
|
+
ranges.push({
|
|
111
|
+
from: match.index,
|
|
112
|
+
to: match.index + colonPos,
|
|
113
|
+
decoration: jsonPropertyMark,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Match string values (strings NOT followed by colon)
|
|
118
|
+
const stringRegex = /"([^"\\]|\\.)*"/g;
|
|
119
|
+
while ((match = stringRegex.exec(doc)) !== null) {
|
|
120
|
+
// Check if this is a property (followed by :) - skip if so
|
|
121
|
+
const afterMatch = doc.slice(match.index + match[0].length).match(/^\s*:/);
|
|
122
|
+
if (!afterMatch) {
|
|
123
|
+
ranges.push({
|
|
124
|
+
from: match.index,
|
|
125
|
+
to: match.index + match[0].length,
|
|
126
|
+
decoration: jsonStringMark,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Match numbers
|
|
132
|
+
const numberRegex = /-?\d+\.?\d*(?:[eE][+-]?\d+)?/g;
|
|
133
|
+
while ((match = numberRegex.exec(doc)) !== null) {
|
|
134
|
+
// Make sure it's not inside a string or template
|
|
135
|
+
const before = doc.slice(0, match.index);
|
|
136
|
+
const inString = (before.match(/"/g) || []).length % 2 === 1;
|
|
137
|
+
const inTemplate =
|
|
138
|
+
before.lastIndexOf("{{") > before.lastIndexOf("}}") &&
|
|
139
|
+
before.includes("{{");
|
|
140
|
+
if (!inString && !inTemplate) {
|
|
141
|
+
ranges.push({
|
|
142
|
+
from: match.index,
|
|
143
|
+
to: match.index + match[0].length,
|
|
144
|
+
decoration: jsonNumberMark,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Match keywords: true, false, null
|
|
150
|
+
const keywordRegex = /\b(true|false|null)\b/g;
|
|
151
|
+
while ((match = keywordRegex.exec(doc)) !== null) {
|
|
152
|
+
const before = doc.slice(0, match.index);
|
|
153
|
+
const inString = (before.match(/"/g) || []).length % 2 === 1;
|
|
154
|
+
if (!inString) {
|
|
155
|
+
ranges.push({
|
|
156
|
+
from: match.index,
|
|
157
|
+
to: match.index + match[0].length,
|
|
158
|
+
decoration: jsonKeywordMark,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Sort by position (must be in order for RangeSetBuilder)
|
|
164
|
+
ranges.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
165
|
+
|
|
166
|
+
// Remove overlaps (templates take priority since they're added first)
|
|
167
|
+
const filtered: DecorationRange[] = [];
|
|
168
|
+
for (const range of ranges) {
|
|
169
|
+
const overlaps = filtered.some(
|
|
170
|
+
(existing) =>
|
|
171
|
+
(range.from >= existing.from && range.from < existing.to) ||
|
|
172
|
+
(range.to > existing.from && range.to <= existing.to),
|
|
173
|
+
);
|
|
174
|
+
if (!overlaps) {
|
|
175
|
+
filtered.push(range);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return filtered;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Calculate indentation for JSON, properly handling template expressions.
|
|
184
|
+
* Counts structural { and [ while ignoring {{ template braces.
|
|
185
|
+
* @internal Exported for testing
|
|
186
|
+
*/
|
|
187
|
+
export function calculateJsonIndentation(
|
|
188
|
+
textBefore: string,
|
|
189
|
+
indentUnit: number,
|
|
190
|
+
): number {
|
|
191
|
+
let depth = 0;
|
|
192
|
+
let insideString = false;
|
|
193
|
+
let i = 0;
|
|
194
|
+
|
|
195
|
+
while (i < textBefore.length) {
|
|
196
|
+
const char = textBefore[i];
|
|
197
|
+
const nextChar = i < textBefore.length - 1 ? textBefore[i + 1] : "";
|
|
198
|
+
const prevChar = i > 0 ? textBefore[i - 1] : "";
|
|
199
|
+
|
|
200
|
+
// Handle string boundaries
|
|
201
|
+
if (char === '"' && prevChar !== "\\") {
|
|
202
|
+
insideString = !insideString;
|
|
203
|
+
i++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!insideString) {
|
|
208
|
+
// Skip template braces {{ and }}
|
|
209
|
+
if (char === "{" && nextChar === "{") {
|
|
210
|
+
i += 2;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (char === "}" && nextChar === "}") {
|
|
214
|
+
i += 2;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Count structural braces
|
|
219
|
+
if (char === "{" || char === "[") {
|
|
220
|
+
depth++;
|
|
221
|
+
} else if (char === "}" || char === "]") {
|
|
222
|
+
depth = Math.max(0, depth - 1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return depth * indentUnit;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* JSON language support for CodeEditor with template expression handling.
|
|
234
|
+
*/
|
|
235
|
+
export const jsonLanguageSupport: LanguageSupport = {
|
|
236
|
+
extension: json(),
|
|
237
|
+
buildDecorations: buildJsonDecorations,
|
|
238
|
+
isValidTemplatePosition: isValidJsonTemplatePosition,
|
|
239
|
+
calculateIndentation: calculateJsonIndentation,
|
|
240
|
+
};
|