@checkstack/ui 0.2.4 → 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 +30 -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/index.ts +1 -1
- package/src/components/TemplateEditor.test.ts +0 -156
- package/src/components/TemplateEditor.tsx +0 -435
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { isValidXmlTemplatePosition, calculateXmlIndentation } from "./xml";
|
|
4
|
+
|
|
5
|
+
describe("isValidXmlTemplatePosition", () => {
|
|
6
|
+
describe("text content positions", () => {
|
|
7
|
+
it("returns true in text between tags", () => {
|
|
8
|
+
expect(isValidXmlTemplatePosition("<tag>hello")).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns true after opening tag on new line", () => {
|
|
12
|
+
expect(isValidXmlTemplatePosition("<tag>\n ")).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns true in text with existing template", () => {
|
|
16
|
+
expect(isValidXmlTemplatePosition("<tag>hello {{name}}")).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns true in empty text position", () => {
|
|
20
|
+
expect(isValidXmlTemplatePosition("<tag>")).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("attribute value positions", () => {
|
|
25
|
+
it("returns true inside double-quoted attribute value", () => {
|
|
26
|
+
expect(isValidXmlTemplatePosition('<tag attr="')).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns true inside single-quoted attribute value", () => {
|
|
30
|
+
expect(isValidXmlTemplatePosition("<tag attr='")).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns true in middle of attribute value", () => {
|
|
34
|
+
expect(isValidXmlTemplatePosition('<tag attr="hello')).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns true in attribute with template", () => {
|
|
38
|
+
expect(isValidXmlTemplatePosition('<tag attr="{{name}}')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("invalid positions", () => {
|
|
43
|
+
it("returns false inside tag name", () => {
|
|
44
|
+
expect(isValidXmlTemplatePosition("<ta")).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns false for empty string", () => {
|
|
48
|
+
expect(isValidXmlTemplatePosition("")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns false in attribute name position", () => {
|
|
52
|
+
expect(isValidXmlTemplatePosition("<tag at")).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns false right after opening bracket", () => {
|
|
56
|
+
expect(isValidXmlTemplatePosition("<")).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("complex nested structures", () => {
|
|
61
|
+
it("returns true in nested tag text content", () => {
|
|
62
|
+
expect(isValidXmlTemplatePosition("<root><child>text")).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns true in deeply nested text", () => {
|
|
66
|
+
expect(isValidXmlTemplatePosition("<a><b><c>")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns true after self-closing tag", () => {
|
|
70
|
+
expect(isValidXmlTemplatePosition("<root><self/>text")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("calculateXmlIndentation", () => {
|
|
76
|
+
const INDENT = 2;
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Basic tag indentation
|
|
80
|
+
// ============================================================================
|
|
81
|
+
describe("basic tag indentation", () => {
|
|
82
|
+
it("returns 0 for empty document", () => {
|
|
83
|
+
expect(calculateXmlIndentation("", INDENT)).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("indents after opening tag (new line)", () => {
|
|
87
|
+
// User typed "<root>" and pressed Enter
|
|
88
|
+
expect(calculateXmlIndentation("<root>\n", INDENT)).toBe(INDENT);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("indents when inside an unclosed opening tag", () => {
|
|
92
|
+
// After an opening tag, depth is 1 even on same line
|
|
93
|
+
expect(calculateXmlIndentation("<root>", INDENT)).toBe(INDENT);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns 0 after self-closing tag", () => {
|
|
97
|
+
expect(calculateXmlIndentation("<self/>\n", INDENT)).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns 0 after closing tag", () => {
|
|
101
|
+
expect(calculateXmlIndentation("</root>\n", INDENT)).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns 0 after complete open-close on same line", () => {
|
|
105
|
+
expect(calculateXmlIndentation("<tag></tag>\n", INDENT)).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Nested structure indentation
|
|
111
|
+
// ============================================================================
|
|
112
|
+
describe("nested structure indentation", () => {
|
|
113
|
+
it("indents to level 2 after nested opening tag", () => {
|
|
114
|
+
const text = `<root>
|
|
115
|
+
<child>\n`;
|
|
116
|
+
expect(calculateXmlIndentation(text, INDENT)).toBe(2 * INDENT);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("maintains level 1 after nested closing tag", () => {
|
|
120
|
+
const text = `<root>
|
|
121
|
+
<child></child>\n`;
|
|
122
|
+
expect(calculateXmlIndentation(text, INDENT)).toBe(INDENT);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("indents to level 3 for deep nesting", () => {
|
|
126
|
+
const text = `<root>
|
|
127
|
+
<a>
|
|
128
|
+
<b>\n`;
|
|
129
|
+
expect(calculateXmlIndentation(text, INDENT)).toBe(3 * INDENT);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("handles mixed self-closing and regular tags", () => {
|
|
133
|
+
const text = `<root>
|
|
134
|
+
<self/>
|
|
135
|
+
<child>\n`;
|
|
136
|
+
expect(calculateXmlIndentation(text, INDENT)).toBe(2 * INDENT);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Template handling (no interference with structure)
|
|
142
|
+
// ============================================================================
|
|
143
|
+
describe("template handling", () => {
|
|
144
|
+
it("counts depth correctly with template in text content", () => {
|
|
145
|
+
// Template doesn't affect tag depth counting
|
|
146
|
+
expect(calculateXmlIndentation("<tag>{{value}}", INDENT)).toBe(INDENT);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("indents after tag even with template content", () => {
|
|
150
|
+
expect(calculateXmlIndentation("<tag>\n {{value}}\n", INDENT)).toBe(
|
|
151
|
+
INDENT,
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("handles template in attribute value", () => {
|
|
156
|
+
expect(calculateXmlIndentation('<tag attr="{{value}}">\n', INDENT)).toBe(
|
|
157
|
+
INDENT,
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("handles multiple templates", () => {
|
|
162
|
+
const text = `<root>
|
|
163
|
+
<a>{{val1}}</a>
|
|
164
|
+
<b>{{val2}}</b>\n`;
|
|
165
|
+
expect(calculateXmlIndentation(text, INDENT)).toBe(INDENT);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Edge cases
|
|
171
|
+
// ============================================================================
|
|
172
|
+
describe("edge cases", () => {
|
|
173
|
+
it("handles only newline", () => {
|
|
174
|
+
expect(calculateXmlIndentation("\n", INDENT)).toBe(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("handles multiple newlines", () => {
|
|
178
|
+
expect(calculateXmlIndentation("\n\n\n", INDENT)).toBe(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("handles XML declaration", () => {
|
|
182
|
+
expect(calculateXmlIndentation('<?xml version="1.0"?>\n', INDENT)).toBe(
|
|
183
|
+
0,
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("handles tag with attributes", () => {
|
|
188
|
+
expect(calculateXmlIndentation('<tag attr="value">\n', INDENT)).toBe(
|
|
189
|
+
INDENT,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("handles tag with multiple attributes", () => {
|
|
194
|
+
expect(calculateXmlIndentation('<tag a="1" b="2" c="3">\n', INDENT)).toBe(
|
|
195
|
+
INDENT,
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("handles CDATA section (counts parent tag)", () => {
|
|
200
|
+
// CDATA is inside an unclosed tag
|
|
201
|
+
expect(calculateXmlIndentation("<tag><![CDATA[", INDENT)).toBe(INDENT);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("handles comments", () => {
|
|
205
|
+
expect(calculateXmlIndentation("<!-- comment -->\n", INDENT)).toBe(0);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Real-world document structures
|
|
211
|
+
// ============================================================================
|
|
212
|
+
describe("real-world document structures", () => {
|
|
213
|
+
it("indents HTML-like structure", () => {
|
|
214
|
+
const text = `<html>
|
|
215
|
+
<head>
|
|
216
|
+
<title>Test</title>
|
|
217
|
+
</head>
|
|
218
|
+
<body>\n`;
|
|
219
|
+
expect(calculateXmlIndentation(text, INDENT)).toBe(2 * INDENT);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("handles configuration-style XML", () => {
|
|
223
|
+
const text = `<config>
|
|
224
|
+
<database>
|
|
225
|
+
<host>\n`;
|
|
226
|
+
expect(calculateXmlIndentation(text, INDENT)).toBe(3 * INDENT);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("handles SOAP-style envelope", () => {
|
|
230
|
+
const text = `<soap:Envelope>
|
|
231
|
+
<soap:Body>
|
|
232
|
+
<Request>\n`;
|
|
233
|
+
expect(calculateXmlIndentation(text, INDENT)).toBe(3 * INDENT);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { xml } from "@codemirror/lang-xml";
|
|
2
|
+
import { Decoration } from "@codemirror/view";
|
|
3
|
+
import type { LanguageSupport, DecorationRange } from "./types";
|
|
4
|
+
|
|
5
|
+
// Decoration marks for XML syntax highlighting using inline styles
|
|
6
|
+
const xmlTagMark = Decoration.mark({
|
|
7
|
+
attributes: { style: "color: hsl(280, 65%, 60%)" },
|
|
8
|
+
});
|
|
9
|
+
const xmlAttrNameMark = Decoration.mark({
|
|
10
|
+
attributes: { style: "color: hsl(190, 70%, 50%)" },
|
|
11
|
+
});
|
|
12
|
+
const xmlAttrValueMark = Decoration.mark({
|
|
13
|
+
attributes: { style: "color: hsl(142.1, 76.2%, 36.3%)" },
|
|
14
|
+
});
|
|
15
|
+
const templateMark = Decoration.mark({
|
|
16
|
+
attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if cursor is in a valid XML template position.
|
|
21
|
+
* Templates can appear in:
|
|
22
|
+
* 1. Text content between tags: <tag>|</tag>
|
|
23
|
+
* 2. Attribute values: <tag attr="|">
|
|
24
|
+
* @internal Exported for testing
|
|
25
|
+
*/
|
|
26
|
+
export function isValidXmlTemplatePosition(text: string): boolean {
|
|
27
|
+
// Check if we're inside an attribute value (inside quotes after =)
|
|
28
|
+
let inTag = false;
|
|
29
|
+
let inAttrValue = false;
|
|
30
|
+
let quoteChar = "";
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < text.length; i++) {
|
|
33
|
+
const char = text[i];
|
|
34
|
+
|
|
35
|
+
if (!inTag && char === "<" && text[i + 1] !== "/") {
|
|
36
|
+
inTag = true;
|
|
37
|
+
inAttrValue = false;
|
|
38
|
+
} else if (inTag && char === ">") {
|
|
39
|
+
inTag = false;
|
|
40
|
+
inAttrValue = false;
|
|
41
|
+
} else if (inTag && !inAttrValue && char === "=") {
|
|
42
|
+
// Next should be a quote
|
|
43
|
+
const nextChar = text[i + 1];
|
|
44
|
+
if (nextChar === '"' || nextChar === "'") {
|
|
45
|
+
quoteChar = nextChar;
|
|
46
|
+
inAttrValue = true;
|
|
47
|
+
i++; // Skip the quote
|
|
48
|
+
}
|
|
49
|
+
} else if (inAttrValue && char === quoteChar) {
|
|
50
|
+
inAttrValue = false;
|
|
51
|
+
quoteChar = "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Valid if inside attribute value
|
|
56
|
+
if (inAttrValue) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Valid if between tags (text content position)
|
|
61
|
+
// Check if we're after a closing > and not inside an opening <
|
|
62
|
+
const lastOpenTag = text.lastIndexOf("<");
|
|
63
|
+
const lastCloseTag = text.lastIndexOf(">");
|
|
64
|
+
|
|
65
|
+
// If last > is after last <, we're in text content
|
|
66
|
+
if (lastCloseTag > lastOpenTag) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build XML + template decorations.
|
|
75
|
+
*/
|
|
76
|
+
function buildXmlDecorations(doc: string): DecorationRange[] {
|
|
77
|
+
const ranges: DecorationRange[] = [];
|
|
78
|
+
|
|
79
|
+
// Match templates first (highest priority)
|
|
80
|
+
const templateRegex = /\{\{[\w.[\]]*\}\}/g;
|
|
81
|
+
let match;
|
|
82
|
+
while ((match = templateRegex.exec(doc)) !== null) {
|
|
83
|
+
ranges.push({
|
|
84
|
+
from: match.index,
|
|
85
|
+
to: match.index + match[0].length,
|
|
86
|
+
decoration: templateMark,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Match XML tags (opening and closing)
|
|
91
|
+
const tagRegex = /<\/?[\w:-]+/g;
|
|
92
|
+
while ((match = tagRegex.exec(doc)) !== null) {
|
|
93
|
+
ranges.push({
|
|
94
|
+
from: match.index,
|
|
95
|
+
to: match.index + match[0].length,
|
|
96
|
+
decoration: xmlTagMark,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Match closing > and />
|
|
101
|
+
const closeTagRegex = /\/?>/g;
|
|
102
|
+
while ((match = closeTagRegex.exec(doc)) !== null) {
|
|
103
|
+
ranges.push({
|
|
104
|
+
from: match.index,
|
|
105
|
+
to: match.index + match[0].length,
|
|
106
|
+
decoration: xmlTagMark,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Match attribute names
|
|
111
|
+
const attrNameRegex = /\s([\w:-]+)=/g;
|
|
112
|
+
while ((match = attrNameRegex.exec(doc)) !== null) {
|
|
113
|
+
ranges.push({
|
|
114
|
+
from: match.index + 1, // Skip leading space
|
|
115
|
+
to: match.index + 1 + match[1].length,
|
|
116
|
+
decoration: xmlAttrNameMark,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Match attribute values (quoted strings after =)
|
|
121
|
+
const attrValueRegex = /=(["'])(?:(?!\1)[^\\]|\\.)*\1/g;
|
|
122
|
+
while ((match = attrValueRegex.exec(doc)) !== null) {
|
|
123
|
+
// Skip the = but include the quotes
|
|
124
|
+
ranges.push({
|
|
125
|
+
from: match.index + 1,
|
|
126
|
+
to: match.index + match[0].length,
|
|
127
|
+
decoration: xmlAttrValueMark,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Sort by position
|
|
132
|
+
ranges.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
133
|
+
|
|
134
|
+
// Remove overlaps (templates take priority)
|
|
135
|
+
const filtered: DecorationRange[] = [];
|
|
136
|
+
for (const range of ranges) {
|
|
137
|
+
const overlaps = filtered.some(
|
|
138
|
+
(existing) =>
|
|
139
|
+
(range.from >= existing.from && range.from < existing.to) ||
|
|
140
|
+
(range.to > existing.from && range.to <= existing.to),
|
|
141
|
+
);
|
|
142
|
+
if (!overlaps) {
|
|
143
|
+
filtered.push(range);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return filtered;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Calculate indentation for XML, properly handling template expressions.
|
|
152
|
+
* @internal Exported for testing
|
|
153
|
+
*/
|
|
154
|
+
export function calculateXmlIndentation(
|
|
155
|
+
textBefore: string,
|
|
156
|
+
indentUnit: number,
|
|
157
|
+
): number {
|
|
158
|
+
let depth = 0;
|
|
159
|
+
|
|
160
|
+
// Match all opening and closing tags
|
|
161
|
+
const tagRegex = /<\/?[\w:-]+[^>]*\/?>/g;
|
|
162
|
+
let match;
|
|
163
|
+
|
|
164
|
+
while ((match = tagRegex.exec(textBefore)) !== null) {
|
|
165
|
+
const tag = match[0];
|
|
166
|
+
|
|
167
|
+
// Self-closing tag: <tag />
|
|
168
|
+
if (tag.endsWith("/>")) {
|
|
169
|
+
// No depth change
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Closing tag: </tag>
|
|
174
|
+
if (tag.startsWith("</")) {
|
|
175
|
+
depth = Math.max(0, depth - 1);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Opening tag: <tag> or <tag attr="value">
|
|
180
|
+
depth++;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return depth * indentUnit;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* XML language support for CodeEditor with template expression handling.
|
|
188
|
+
*/
|
|
189
|
+
export const xmlLanguageSupport: LanguageSupport = {
|
|
190
|
+
extension: xml(),
|
|
191
|
+
buildDecorations: buildXmlDecorations,
|
|
192
|
+
isValidTemplatePosition: isValidXmlTemplatePosition,
|
|
193
|
+
calculateIndentation: calculateXmlIndentation,
|
|
194
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { isValidYamlTemplatePosition, calculateYamlIndentation } from "./yaml";
|
|
4
|
+
|
|
5
|
+
describe("isValidYamlTemplatePosition", () => {
|
|
6
|
+
describe("value positions", () => {
|
|
7
|
+
it("returns true after 'key: '", () => {
|
|
8
|
+
expect(isValidYamlTemplatePosition("name: ")).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns true after 'key: ' with existing value", () => {
|
|
12
|
+
expect(isValidYamlTemplatePosition("name: hello")).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns true in list item position", () => {
|
|
16
|
+
expect(isValidYamlTemplatePosition("- ")).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns true inside double-quoted string", () => {
|
|
20
|
+
expect(isValidYamlTemplatePosition('name: "hello')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns true inside single-quoted string", () => {
|
|
24
|
+
expect(isValidYamlTemplatePosition("name: 'hello")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("invalid positions", () => {
|
|
29
|
+
it("returns false for key position", () => {
|
|
30
|
+
expect(isValidYamlTemplatePosition("name")).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns false for empty string", () => {
|
|
34
|
+
expect(isValidYamlTemplatePosition("")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("calculateYamlIndentation", () => {
|
|
40
|
+
const INDENT = 2;
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Basic YAML key:value patterns
|
|
44
|
+
// ============================================================================
|
|
45
|
+
describe("basic key:value indentation", () => {
|
|
46
|
+
it("returns 0 for empty document", () => {
|
|
47
|
+
expect(calculateYamlIndentation("", INDENT)).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("indents after key ending with colon (new line)", () => {
|
|
51
|
+
// User typed "test:" and pressed Enter
|
|
52
|
+
// textBefore = "test:\n" (cursor at start of empty new line)
|
|
53
|
+
expect(calculateYamlIndentation("test:\n", INDENT)).toBe(INDENT);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("indents after key with trailing whitespace", () => {
|
|
57
|
+
expect(calculateYamlIndentation("test: \n", INDENT)).toBe(INDENT);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("maintains 0 indent for same-line content after key", () => {
|
|
61
|
+
// User is typing on the line "test:" without pressing Enter
|
|
62
|
+
expect(calculateYamlIndentation("test:", INDENT)).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("maintains 0 indent for key:value on same line", () => {
|
|
66
|
+
expect(calculateYamlIndentation("test: value", INDENT)).toBe(0);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Nested object indentation
|
|
72
|
+
// ============================================================================
|
|
73
|
+
describe("nested object indentation", () => {
|
|
74
|
+
it("indents to level 2 after nested key", () => {
|
|
75
|
+
const text = `parent:
|
|
76
|
+
child:\n`;
|
|
77
|
+
expect(calculateYamlIndentation(text, INDENT)).toBe(2 * INDENT);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("maintains level 1 after nested key:value", () => {
|
|
81
|
+
const text = `parent:
|
|
82
|
+
child: value\n`;
|
|
83
|
+
expect(calculateYamlIndentation(text, INDENT)).toBe(INDENT);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("indents to level 3 for deep nesting", () => {
|
|
87
|
+
const text = `level1:
|
|
88
|
+
level2:
|
|
89
|
+
level3:\n`;
|
|
90
|
+
expect(calculateYamlIndentation(text, INDENT)).toBe(3 * INDENT);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns to level 1 after deep nesting with value", () => {
|
|
94
|
+
const text = `level1:
|
|
95
|
+
level2:
|
|
96
|
+
level3: value\n`;
|
|
97
|
+
expect(calculateYamlIndentation(text, INDENT)).toBe(2 * INDENT);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// List item indentation
|
|
103
|
+
// ============================================================================
|
|
104
|
+
describe("list item indentation", () => {
|
|
105
|
+
it("indents after list item", () => {
|
|
106
|
+
expect(calculateYamlIndentation("- item\n", INDENT)).toBe(INDENT);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("indents after list item with nested key", () => {
|
|
110
|
+
const text = `items:
|
|
111
|
+
- name: test\n`;
|
|
112
|
+
expect(calculateYamlIndentation(text, INDENT)).toBe(2 * INDENT);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("indents after empty list marker", () => {
|
|
116
|
+
expect(calculateYamlIndentation("-\n", INDENT)).toBe(INDENT);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("handles list with object items", () => {
|
|
120
|
+
const text = `items:
|
|
121
|
+
- name: test
|
|
122
|
+
value: 123\n`;
|
|
123
|
+
expect(calculateYamlIndentation(text, INDENT)).toBe(2 * INDENT);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Template handling (no interference with structure)
|
|
129
|
+
// ============================================================================
|
|
130
|
+
describe("template handling", () => {
|
|
131
|
+
it("maintains indentation with template value on same line", () => {
|
|
132
|
+
expect(calculateYamlIndentation("name: {{value}}", INDENT)).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("indents after key even when value contains template", () => {
|
|
136
|
+
expect(calculateYamlIndentation("name: {{value}}\n", INDENT)).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("maintains nesting with template in nested value", () => {
|
|
140
|
+
const text = `parent:
|
|
141
|
+
child: {{value}}\n`;
|
|
142
|
+
expect(calculateYamlIndentation(text, INDENT)).toBe(INDENT);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("indents after key with template but ending with colon", () => {
|
|
146
|
+
const text = `parent:
|
|
147
|
+
child:\n`;
|
|
148
|
+
expect(calculateYamlIndentation(text, INDENT)).toBe(2 * INDENT);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Multi-line string handling
|
|
154
|
+
// ============================================================================
|
|
155
|
+
describe("multi-line scenarios", () => {
|
|
156
|
+
it("maintains current indent after blank line", () => {
|
|
157
|
+
const text = `parent:
|
|
158
|
+
child: value
|
|
159
|
+
|
|
160
|
+
`;
|
|
161
|
+
expect(calculateYamlIndentation(text, INDENT)).toBe(INDENT);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("handles multiple blank lines", () => {
|
|
165
|
+
const text = `parent:
|
|
166
|
+
child: value
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
`;
|
|
170
|
+
expect(calculateYamlIndentation(text, INDENT)).toBe(INDENT);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Edge cases
|
|
176
|
+
// ============================================================================
|
|
177
|
+
describe("edge cases", () => {
|
|
178
|
+
it("handles only newline", () => {
|
|
179
|
+
expect(calculateYamlIndentation("\n", INDENT)).toBe(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("handles multiple newlines only", () => {
|
|
183
|
+
expect(calculateYamlIndentation("\n\n\n", INDENT)).toBe(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("handles colon in string value", () => {
|
|
187
|
+
expect(
|
|
188
|
+
calculateYamlIndentation('url: "http://example.com"\n', INDENT),
|
|
189
|
+
).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("handles key with numbers", () => {
|
|
193
|
+
expect(calculateYamlIndentation("key123:\n", INDENT)).toBe(INDENT);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("handles key with hyphens", () => {
|
|
197
|
+
expect(calculateYamlIndentation("my-key:\n", INDENT)).toBe(INDENT);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|