@checkstack/ui 0.5.3 → 1.0.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 +33 -0
- package/package.json +3 -10
- package/src/components/CodeEditor/CodeEditor.tsx +14 -420
- package/src/components/CodeEditor/MonacoEditor.tsx +530 -0
- package/src/components/CodeEditor/generateTypeDefinitions.ts +169 -0
- package/src/components/CodeEditor/index.ts +4 -3
- package/src/components/CodeEditor/templateUtils.test.ts +87 -0
- package/src/components/CodeEditor/templateUtils.ts +81 -0
- package/src/components/DynamicForm/FormField.tsx +13 -7
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +33 -0
- package/src/components/DynamicForm/utils.ts +3 -0
- package/src/components/CodeEditor/languageSupport/enterBehavior.test.ts +0 -173
- package/src/components/CodeEditor/languageSupport/enterBehavior.ts +0 -35
- package/src/components/CodeEditor/languageSupport/index.ts +0 -22
- package/src/components/CodeEditor/languageSupport/json-utils.ts +0 -117
- package/src/components/CodeEditor/languageSupport/json.test.ts +0 -274
- package/src/components/CodeEditor/languageSupport/json.ts +0 -139
- package/src/components/CodeEditor/languageSupport/markdown-utils.ts +0 -65
- package/src/components/CodeEditor/languageSupport/markdown.test.ts +0 -245
- package/src/components/CodeEditor/languageSupport/markdown.ts +0 -134
- package/src/components/CodeEditor/languageSupport/types.ts +0 -48
- package/src/components/CodeEditor/languageSupport/xml-utils.ts +0 -94
- package/src/components/CodeEditor/languageSupport/xml.test.ts +0 -239
- package/src/components/CodeEditor/languageSupport/xml.ts +0 -116
- package/src/components/CodeEditor/languageSupport/yaml-utils.ts +0 -101
- package/src/components/CodeEditor/languageSupport/yaml.test.ts +0 -203
- package/src/components/CodeEditor/languageSupport/yaml.ts +0 -120
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { detectOpenTemplate, detectAutoClosedBraces } from "./templateUtils";
|
|
3
|
+
|
|
4
|
+
describe("detectOpenTemplate", () => {
|
|
5
|
+
it("returns isInTemplate=false when no open template", () => {
|
|
6
|
+
const content = '{"foo": "bar"}';
|
|
7
|
+
const result = detectOpenTemplate({ content, cursorOffset: 10 });
|
|
8
|
+
expect(result.isInTemplate).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns isInTemplate=false when template is closed", () => {
|
|
12
|
+
const content = '{"foo": "{{test}}"}';
|
|
13
|
+
const result = detectOpenTemplate({ content, cursorOffset: 18 });
|
|
14
|
+
expect(result.isInTemplate).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns isInTemplate=true when inside open template", () => {
|
|
18
|
+
const content = '{"foo": "{{payl';
|
|
19
|
+
const result = detectOpenTemplate({
|
|
20
|
+
content,
|
|
21
|
+
cursorOffset: content.length,
|
|
22
|
+
});
|
|
23
|
+
expect(result.isInTemplate).toBe(true);
|
|
24
|
+
expect(result.query).toBe("payl");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns correct startOffset", () => {
|
|
28
|
+
const content = '{"foo": "{{test';
|
|
29
|
+
const result = detectOpenTemplate({
|
|
30
|
+
content,
|
|
31
|
+
cursorOffset: content.length,
|
|
32
|
+
});
|
|
33
|
+
expect(result.startOffset).toBe(9);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns correct line number and column", () => {
|
|
37
|
+
const content = `{
|
|
38
|
+
"foo": "{{test`;
|
|
39
|
+
const result = detectOpenTemplate({
|
|
40
|
+
content,
|
|
41
|
+
cursorOffset: content.length,
|
|
42
|
+
});
|
|
43
|
+
expect(result.lineNumber).toBe(2);
|
|
44
|
+
expect(result.startColumn).toBe(11);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("handles cursor right after {{", () => {
|
|
48
|
+
const content = '{"foo": "{{';
|
|
49
|
+
const result = detectOpenTemplate({
|
|
50
|
+
content,
|
|
51
|
+
cursorOffset: content.length,
|
|
52
|
+
});
|
|
53
|
+
expect(result.isInTemplate).toBe(true);
|
|
54
|
+
expect(result.query).toBe("");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns isInTemplate=false when cursor is after closed and before new open", () => {
|
|
58
|
+
const content = '{"a": "{{done}}", "b": "';
|
|
59
|
+
const result = detectOpenTemplate({
|
|
60
|
+
content,
|
|
61
|
+
cursorOffset: content.length,
|
|
62
|
+
});
|
|
63
|
+
expect(result.isInTemplate).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("detectAutoClosedBraces", () => {
|
|
68
|
+
it("returns 0 when no braces after cursor", () => {
|
|
69
|
+
const content = '{"foo": "{{test}}"}';
|
|
70
|
+
const result = detectAutoClosedBraces({ content, cursorOffset: 9 });
|
|
71
|
+
expect(result).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("returns 2 when }} follows cursor", () => {
|
|
75
|
+
const content = '{"foo": "{{test}}"}';
|
|
76
|
+
// Cursor is after "test" but before }}
|
|
77
|
+
const result = detectAutoClosedBraces({ content, cursorOffset: 15 });
|
|
78
|
+
expect(result).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns 1 when single } follows cursor", () => {
|
|
82
|
+
const content = '{"foo": "{{test}"}';
|
|
83
|
+
// Only one } after test
|
|
84
|
+
const result = detectAutoClosedBraces({ content, cursorOffset: 15 });
|
|
85
|
+
expect(result).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template utilities for Monaco Editor
|
|
3
|
+
* Handles detection and position tracking of {{...}} template syntax
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Information about an unclosed template at the cursor
|
|
8
|
+
*/
|
|
9
|
+
export interface OpenTemplateContext {
|
|
10
|
+
/** Whether we're inside an unclosed {{ */
|
|
11
|
+
isInTemplate: boolean;
|
|
12
|
+
/** The text typed after {{ */
|
|
13
|
+
query: string;
|
|
14
|
+
/** The offset where {{ starts */
|
|
15
|
+
startOffset: number;
|
|
16
|
+
/** The line number where {{ is (1-indexed) */
|
|
17
|
+
lineNumber: number;
|
|
18
|
+
/** The column where {{ starts (1-indexed) */
|
|
19
|
+
startColumn: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detect if cursor is inside an unclosed {{ template context
|
|
24
|
+
*/
|
|
25
|
+
export function detectOpenTemplate({
|
|
26
|
+
content,
|
|
27
|
+
cursorOffset,
|
|
28
|
+
}: {
|
|
29
|
+
content: string;
|
|
30
|
+
cursorOffset: number;
|
|
31
|
+
}): OpenTemplateContext {
|
|
32
|
+
const textBefore = content.slice(0, cursorOffset);
|
|
33
|
+
const lastOpen = textBefore.lastIndexOf("{{");
|
|
34
|
+
const lastClose = textBefore.lastIndexOf("}}");
|
|
35
|
+
|
|
36
|
+
if (lastOpen === -1 || lastOpen <= lastClose) {
|
|
37
|
+
return {
|
|
38
|
+
isInTemplate: false,
|
|
39
|
+
query: "",
|
|
40
|
+
startOffset: -1,
|
|
41
|
+
lineNumber: 0,
|
|
42
|
+
startColumn: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const query = textBefore.slice(lastOpen + 2);
|
|
47
|
+
|
|
48
|
+
// Calculate line number and column
|
|
49
|
+
const beforeTemplate = content.slice(0, lastOpen);
|
|
50
|
+
const lines = beforeTemplate.split("\n");
|
|
51
|
+
const lineNumber = lines.length;
|
|
52
|
+
const startColumn = (lines.at(-1)?.length ?? 0) + 1;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
isInTemplate: true,
|
|
56
|
+
query,
|
|
57
|
+
startOffset: lastOpen,
|
|
58
|
+
lineNumber,
|
|
59
|
+
startColumn,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if there are auto-closed braces after the cursor
|
|
65
|
+
*/
|
|
66
|
+
export function detectAutoClosedBraces({
|
|
67
|
+
content,
|
|
68
|
+
cursorOffset,
|
|
69
|
+
}: {
|
|
70
|
+
content: string;
|
|
71
|
+
cursorOffset: number;
|
|
72
|
+
}): number {
|
|
73
|
+
const textAfter = content.slice(cursorOffset);
|
|
74
|
+
if (textAfter.startsWith("}}")) {
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
if (textAfter.startsWith("}")) {
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
@@ -349,7 +349,18 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
349
349
|
if (!itemSchema) return <></>;
|
|
350
350
|
|
|
351
351
|
// Helper to create initial value for new array items
|
|
352
|
-
const createNewItem = ():
|
|
352
|
+
const createNewItem = (): unknown => {
|
|
353
|
+
// Handle primitive types
|
|
354
|
+
if (itemSchema.type === "string") {
|
|
355
|
+
return itemSchema.default ?? "";
|
|
356
|
+
}
|
|
357
|
+
if (itemSchema.type === "number" || itemSchema.type === "integer") {
|
|
358
|
+
return itemSchema.default ?? 0;
|
|
359
|
+
}
|
|
360
|
+
if (itemSchema.type === "boolean") {
|
|
361
|
+
return itemSchema.default ?? false;
|
|
362
|
+
}
|
|
363
|
+
|
|
353
364
|
// Check if itemSchema is a discriminated union
|
|
354
365
|
const variants = itemSchema.oneOf || itemSchema.anyOf;
|
|
355
366
|
if (variants && variants.length > 0) {
|
|
@@ -391,12 +402,7 @@ export const FormField: React.FC<FormFieldProps> = ({
|
|
|
391
402
|
type="button"
|
|
392
403
|
variant="outline"
|
|
393
404
|
size="sm"
|
|
394
|
-
onClick={() =>
|
|
395
|
-
onChange([
|
|
396
|
-
...(items as Record<string, unknown>[]),
|
|
397
|
-
createNewItem(),
|
|
398
|
-
])
|
|
399
|
-
}
|
|
405
|
+
onClick={() => onChange([...items, createNewItem()])}
|
|
400
406
|
className="h-8 gap-1 transition-all hover:bg-accent hover:text-accent-foreground"
|
|
401
407
|
>
|
|
402
408
|
<Plus className="h-4 w-4" />
|
|
@@ -233,6 +233,39 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
|
|
|
233
233
|
templateProperties={templateProperties}
|
|
234
234
|
/>
|
|
235
235
|
)}
|
|
236
|
+
|
|
237
|
+
{selectedType === "javascript" && (
|
|
238
|
+
<CodeEditor
|
|
239
|
+
id={id}
|
|
240
|
+
value={value ?? ""}
|
|
241
|
+
onChange={onChange}
|
|
242
|
+
language="javascript"
|
|
243
|
+
minHeight="150px"
|
|
244
|
+
templateProperties={templateProperties}
|
|
245
|
+
/>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{selectedType === "typescript" && (
|
|
249
|
+
<CodeEditor
|
|
250
|
+
id={id}
|
|
251
|
+
value={value ?? ""}
|
|
252
|
+
onChange={onChange}
|
|
253
|
+
language="typescript"
|
|
254
|
+
minHeight="150px"
|
|
255
|
+
templateProperties={templateProperties}
|
|
256
|
+
/>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
{selectedType === "shell" && (
|
|
260
|
+
<CodeEditor
|
|
261
|
+
id={id}
|
|
262
|
+
value={value ?? ""}
|
|
263
|
+
onChange={onChange}
|
|
264
|
+
language="shell"
|
|
265
|
+
minHeight="150px"
|
|
266
|
+
templateProperties={templateProperties}
|
|
267
|
+
/>
|
|
268
|
+
)}
|
|
236
269
|
</div>
|
|
237
270
|
);
|
|
238
271
|
};
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { isBetweenBrackets } from "./enterBehavior";
|
|
4
|
-
|
|
5
|
-
describe("isBetweenBrackets", () => {
|
|
6
|
-
// ============================================================================
|
|
7
|
-
// Curly braces
|
|
8
|
-
// ============================================================================
|
|
9
|
-
describe("curly braces", () => {
|
|
10
|
-
it("returns true for empty object {|}", () => {
|
|
11
|
-
expect(isBetweenBrackets("{", "}")).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("returns false when content before closing brace", () => {
|
|
15
|
-
// Last char is "1", not "{"
|
|
16
|
-
expect(isBetweenBrackets('{"a": 1', "}")).toBe(false);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("returns false when only opening brace before", () => {
|
|
20
|
-
expect(isBetweenBrackets("{", "x")).toBe(false);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("returns false when only closing brace after", () => {
|
|
24
|
-
expect(isBetweenBrackets("x", "}")).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("returns false for nested opening {|{", () => {
|
|
28
|
-
expect(isBetweenBrackets("{", "{")).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// ============================================================================
|
|
33
|
-
// Square brackets
|
|
34
|
-
// ============================================================================
|
|
35
|
-
describe("square brackets", () => {
|
|
36
|
-
it("returns true for empty array [|]", () => {
|
|
37
|
-
expect(isBetweenBrackets("[", "]")).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("returns false when content before closing bracket", () => {
|
|
41
|
-
// Last char is "2", not "["
|
|
42
|
-
expect(isBetweenBrackets("[1, 2", "]")).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("returns false when only opening bracket before", () => {
|
|
46
|
-
expect(isBetweenBrackets("[", "x")).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("returns false when only closing bracket after", () => {
|
|
50
|
-
expect(isBetweenBrackets("x", "]")).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("returns false for nested opening [|[", () => {
|
|
54
|
-
expect(isBetweenBrackets("[", "[")).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// ============================================================================
|
|
59
|
-
// XML/HTML tags
|
|
60
|
-
// ============================================================================
|
|
61
|
-
describe("XML/HTML tags", () => {
|
|
62
|
-
it("returns true for empty tag <div>|</div>", () => {
|
|
63
|
-
expect(isBetweenBrackets("<div>", "</div>")).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('returns true for tag with attributes <div class="x">|</div>', () => {
|
|
67
|
-
expect(isBetweenBrackets('<div class="x">', "</div>")).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("returns true for nested tags <outer><inner>|</inner>", () => {
|
|
71
|
-
expect(isBetweenBrackets("<outer><inner>", "</inner>")).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("returns true for self-named closing tag <a>|</a>", () => {
|
|
75
|
-
expect(isBetweenBrackets("<a>", "</a>")).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("returns false for adjacent opening tags <a>|<b>", () => {
|
|
79
|
-
expect(isBetweenBrackets("<a>", "<b>")).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("returns false for text after tag <div>|text", () => {
|
|
83
|
-
expect(isBetweenBrackets("<div>", "text")).toBe(false);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("returns false for self-closing tag <br/>|", () => {
|
|
87
|
-
expect(isBetweenBrackets("<br/>", "")).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("returns false for comment after tag <div>|<!--", () => {
|
|
91
|
-
expect(isBetweenBrackets("<div>", "<!--")).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// ============================================================================
|
|
96
|
-
// Edge cases
|
|
97
|
-
// ============================================================================
|
|
98
|
-
describe("edge cases", () => {
|
|
99
|
-
it("returns false for empty strings", () => {
|
|
100
|
-
expect(isBetweenBrackets("", "")).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("returns false for whitespace only", () => {
|
|
104
|
-
expect(isBetweenBrackets(" ", " ")).toBe(false);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("returns false for newlines", () => {
|
|
108
|
-
expect(isBetweenBrackets("{\n", "\n}")).toBe(false);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("returns true for template braces {{|}}", () => {
|
|
112
|
-
// { followed by } - pattern matches even though it's part of template
|
|
113
|
-
expect(isBetweenBrackets("{{", "}}")).toBe(true);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("handles single character inputs", () => {
|
|
117
|
-
expect(isBetweenBrackets("a", "b")).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// ============================================================================
|
|
122
|
-
// Real-world scenarios
|
|
123
|
-
// ============================================================================
|
|
124
|
-
describe("real-world scenarios", () => {
|
|
125
|
-
it("JSON: empty object literal", () => {
|
|
126
|
-
expect(isBetweenBrackets('{"name": ', "}")).toBe(false); // Not at the boundary
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("JSON: cursor right after opening brace", () => {
|
|
130
|
-
expect(isBetweenBrackets("{", ' "name": "value"}')).toBe(false); // Has content
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("XML: HTML doctype before tag", () => {
|
|
134
|
-
expect(isBetweenBrackets("<!DOCTYPE html><html>", "</html>")).toBe(true);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("XML: after text content in tag", () => {
|
|
138
|
-
expect(isBetweenBrackets("<p>Hello", "</p>")).toBe(false); // "o" != ">"
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("YAML: no brackets involved", () => {
|
|
142
|
-
expect(isBetweenBrackets("key:", "")).toBe(false);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// ============================================================================
|
|
147
|
-
// Regression tests
|
|
148
|
-
// ============================================================================
|
|
149
|
-
describe("regression: autocomplete interaction", () => {
|
|
150
|
-
/**
|
|
151
|
-
* REGRESSION TEST (documented, requires DOM testing)
|
|
152
|
-
*
|
|
153
|
-
* Issue: When autocomplete popup is showing, pressing Enter should select
|
|
154
|
-
* the completion item, NOT insert a newline.
|
|
155
|
-
*
|
|
156
|
-
* Fix: In CodeEditor.tsx, the custom Enter keymap checks `completionStatus(state)`
|
|
157
|
-
* and returns `false` when autocomplete is "active", allowing the autocomplete
|
|
158
|
-
* extension to handle the Enter key.
|
|
159
|
-
*
|
|
160
|
-
* This cannot be unit tested without a full CodeMirror DOM integration.
|
|
161
|
-
* Manual verification steps:
|
|
162
|
-
* 1. Type "{{" in the editor with template properties configured
|
|
163
|
-
* 2. Autocomplete popup should appear
|
|
164
|
-
* 3. Press Enter to select a template
|
|
165
|
-
* 4. Template should be inserted (NOT a newline)
|
|
166
|
-
*/
|
|
167
|
-
it("documents the autocomplete Enter key requirement", () => {
|
|
168
|
-
// This test exists purely as documentation
|
|
169
|
-
// The actual fix is in CodeEditor.tsx: completionStatus(state) check
|
|
170
|
-
expect(true).toBe(true);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
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";
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure utility functions for JSON parsing that don't depend on CodeMirror.
|
|
3
|
-
* These are extracted to allow testing without triggering CodeMirror's module loading.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Check if cursor is in a valid JSON template position.
|
|
8
|
-
* Returns true if:
|
|
9
|
-
* 1. Inside a string value (for string templates like "{{payload.name}}")
|
|
10
|
-
* 2. In a value position after a colon but outside quotes (for number templates like {{payload.count}})
|
|
11
|
-
* 3. Inside an array element position
|
|
12
|
-
*
|
|
13
|
-
* @internal Exported for testing
|
|
14
|
-
*/
|
|
15
|
-
export function isValidJsonTemplatePosition(text: string): boolean {
|
|
16
|
-
let insideString = false;
|
|
17
|
-
let inValuePosition = false;
|
|
18
|
-
// Stack to track object/array nesting: 'o' for object, 'a' for array
|
|
19
|
-
const nestingStack: ("o" | "a")[] = [];
|
|
20
|
-
|
|
21
|
-
for (let i = 0; i < text.length; i++) {
|
|
22
|
-
const char = text[i];
|
|
23
|
-
const prevChar = i > 0 ? text[i - 1] : "";
|
|
24
|
-
|
|
25
|
-
if (char === '"' && prevChar !== "\\") {
|
|
26
|
-
insideString = !insideString;
|
|
27
|
-
} else if (!insideString) {
|
|
28
|
-
switch (char) {
|
|
29
|
-
case ":": {
|
|
30
|
-
inValuePosition = true;
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
case ",": {
|
|
34
|
-
// After comma: in arrays we stay in value position, in objects we go to key position
|
|
35
|
-
const currentContext = nestingStack.at(-1);
|
|
36
|
-
inValuePosition = currentContext === "a";
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
case "{": {
|
|
40
|
-
// Start of object - next is a key position, not a value
|
|
41
|
-
nestingStack.push("o");
|
|
42
|
-
inValuePosition = false;
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
case "[": {
|
|
46
|
-
// Start of array - next is a value position
|
|
47
|
-
nestingStack.push("a");
|
|
48
|
-
inValuePosition = true;
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
case "}": {
|
|
52
|
-
nestingStack.pop();
|
|
53
|
-
inValuePosition = false;
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
case "]": {
|
|
57
|
-
nestingStack.pop();
|
|
58
|
-
inValuePosition = false;
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Valid if inside a string OR in a value position (for bare number templates)
|
|
66
|
-
return insideString || inValuePosition;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Calculate indentation for JSON, properly handling template expressions.
|
|
71
|
-
* Counts structural { and [ while ignoring {{ template braces.
|
|
72
|
-
* @internal Exported for testing
|
|
73
|
-
*/
|
|
74
|
-
export function calculateJsonIndentation(
|
|
75
|
-
textBefore: string,
|
|
76
|
-
indentUnit: number,
|
|
77
|
-
): number {
|
|
78
|
-
let depth = 0;
|
|
79
|
-
let insideString = false;
|
|
80
|
-
let i = 0;
|
|
81
|
-
|
|
82
|
-
while (i < textBefore.length) {
|
|
83
|
-
const char = textBefore[i];
|
|
84
|
-
const nextChar = i < textBefore.length - 1 ? textBefore[i + 1] : "";
|
|
85
|
-
const prevChar = i > 0 ? textBefore[i - 1] : "";
|
|
86
|
-
|
|
87
|
-
// Handle string boundaries
|
|
88
|
-
if (char === '"' && prevChar !== "\\") {
|
|
89
|
-
insideString = !insideString;
|
|
90
|
-
i++;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!insideString) {
|
|
95
|
-
// Skip template braces {{ and }}
|
|
96
|
-
if (char === "{" && nextChar === "{") {
|
|
97
|
-
i += 2;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (char === "}" && nextChar === "}") {
|
|
101
|
-
i += 2;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Count structural braces
|
|
106
|
-
if (char === "{" || char === "[") {
|
|
107
|
-
depth++;
|
|
108
|
-
} else if (char === "}" || char === "]") {
|
|
109
|
-
depth = Math.max(0, depth - 1);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
i++;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return depth * indentUnit;
|
|
117
|
-
}
|