@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +10 -3
  3. package/src/components/CodeEditor/CodeEditor.tsx +420 -0
  4. package/src/components/CodeEditor/index.ts +10 -0
  5. package/src/components/CodeEditor/languageSupport/enterBehavior.test.ts +173 -0
  6. package/src/components/CodeEditor/languageSupport/enterBehavior.ts +35 -0
  7. package/src/components/CodeEditor/languageSupport/index.ts +22 -0
  8. package/src/components/CodeEditor/languageSupport/json.test.ts +271 -0
  9. package/src/components/CodeEditor/languageSupport/json.ts +240 -0
  10. package/src/components/CodeEditor/languageSupport/markdown.test.ts +245 -0
  11. package/src/components/CodeEditor/languageSupport/markdown.ts +183 -0
  12. package/src/components/CodeEditor/languageSupport/types.ts +48 -0
  13. package/src/components/CodeEditor/languageSupport/xml.test.ts +236 -0
  14. package/src/components/CodeEditor/languageSupport/xml.ts +194 -0
  15. package/src/components/CodeEditor/languageSupport/yaml.test.ts +200 -0
  16. package/src/components/CodeEditor/languageSupport/yaml.ts +205 -0
  17. package/src/components/DynamicForm/DynamicForm.tsx +2 -24
  18. package/src/components/DynamicForm/DynamicOptionsField.tsx +48 -21
  19. package/src/components/DynamicForm/FormField.tsx +38 -70
  20. package/src/components/DynamicForm/JsonField.tsx +19 -25
  21. package/src/components/DynamicForm/KeyValueEditor.tsx +314 -0
  22. package/src/components/DynamicForm/MultiTypeEditorField.tsx +465 -0
  23. package/src/components/DynamicForm/index.ts +13 -0
  24. package/src/components/DynamicForm/types.ts +14 -8
  25. package/src/components/DynamicForm/utils.test.ts +390 -0
  26. package/src/components/DynamicForm/utils.ts +142 -3
  27. package/src/index.ts +1 -1
  28. package/src/components/TemplateEditor.test.ts +0 -156
  29. 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
+ });