@checkstack/ui 0.3.0 → 0.3.1

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @checkstack/ui
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [8a87cd4]
8
+ - @checkstack/common@0.5.0
9
+ - @checkstack/frontend-api@0.3.2
10
+
3
11
  ## 0.3.0
4
12
 
5
13
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "dependencies": {
@@ -0,0 +1,117 @@
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
+ }
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
- import { isValidJsonTemplatePosition, calculateJsonIndentation } from "./json";
3
+ import {
4
+ isValidJsonTemplatePosition,
5
+ calculateJsonIndentation,
6
+ } from "./json-utils";
4
7
 
5
8
  describe("isValidJsonTemplatePosition", () => {
6
9
  describe("string values (inside quotes)", () => {
@@ -2,6 +2,12 @@ import { json } from "@codemirror/lang-json";
2
2
  import { Decoration } from "@codemirror/view";
3
3
  import type { LanguageSupport, DecorationRange } from "./types";
4
4
 
5
+ // Re-export pure utils for backwards compatibility
6
+ export {
7
+ isValidJsonTemplatePosition,
8
+ calculateJsonIndentation,
9
+ } from "./json-utils";
10
+
5
11
  // Decoration marks for JSON syntax highlighting using inline styles
6
12
  // (higher specificity than Lezer parser's CSS classes)
7
13
  const jsonStringMark = Decoration.mark({
@@ -20,69 +26,6 @@ const templateMark = Decoration.mark({
20
26
  attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
21
27
  });
22
28
 
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
29
  /**
87
30
  * Build comprehensive JSON + template decorations.
88
31
  * This uses regex-based matching to apply consistent syntax highlighting
@@ -179,55 +122,11 @@ function buildJsonDecorations(doc: string): DecorationRange[] {
179
122
  return filtered;
180
123
  }
181
124
 
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
- }
125
+ // Import the pure utils for use in the language support object
126
+ import {
127
+ isValidJsonTemplatePosition,
128
+ calculateJsonIndentation,
129
+ } from "./json-utils";
231
130
 
232
131
  /**
233
132
  * JSON language support for CodeEditor with template expression handling.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pure utility functions for Markdown 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 Markdown template position.
8
+ * Templates can appear almost anywhere in Markdown (text content).
9
+ * @internal Exported for testing
10
+ */
11
+ export function isValidMarkdownTemplatePosition(text: string): boolean {
12
+ // In Markdown, templates are valid almost everywhere except:
13
+ // - Inside code blocks (``` ... ```)
14
+ // - Inside inline code (` ... `)
15
+
16
+ // Check for unclosed code blocks
17
+ const codeBlockMatches = text.match(/```/g) || [];
18
+ if (codeBlockMatches.length % 2 === 1) {
19
+ return false; // Inside a code block
20
+ }
21
+
22
+ // Check if we're inside inline code on the current line
23
+ const lastNewline = text.lastIndexOf("\n");
24
+ const currentLine = text.slice(lastNewline + 1);
25
+ const backticks = (currentLine.match(/`/g) || []).length;
26
+
27
+ // If odd number of backticks, we're inside inline code
28
+ if (backticks % 2 === 1) {
29
+ return false;
30
+ }
31
+
32
+ // Valid in all other positions
33
+ return true;
34
+ }
35
+
36
+ /**
37
+ * Calculate indentation for Markdown.
38
+ * Markdown uses list indentation.
39
+ * @internal Exported for testing
40
+ */
41
+ export function calculateMarkdownIndentation(
42
+ textBefore: string,
43
+ _indentUnit: number,
44
+ ): number {
45
+ const lines = textBefore.split("\n");
46
+ if (lines.length === 0) return 0;
47
+
48
+ // Find the last non-empty line
49
+ for (let i = lines.length - 1; i >= 0; i--) {
50
+ const line = lines[i];
51
+ if (line.trim().length === 0) continue;
52
+
53
+ // Count leading spaces
54
+ const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
55
+
56
+ // If line is a list item, next line should be indented if continuing
57
+ if (/^\s*[-*+]\s/.test(line) || /^\s*\d+\.\s/.test(line)) {
58
+ return leadingSpaces;
59
+ }
60
+
61
+ return leadingSpaces;
62
+ }
63
+
64
+ return 0;
65
+ }
@@ -3,7 +3,7 @@ import { describe, expect, it } from "bun:test";
3
3
  import {
4
4
  isValidMarkdownTemplatePosition,
5
5
  calculateMarkdownIndentation,
6
- } from "./markdown";
6
+ } from "./markdown-utils";
7
7
 
8
8
  describe("isValidMarkdownTemplatePosition", () => {
9
9
  describe("valid text positions", () => {
@@ -2,6 +2,12 @@ import { markdown } from "@codemirror/lang-markdown";
2
2
  import { Decoration } from "@codemirror/view";
3
3
  import type { LanguageSupport, DecorationRange } from "./types";
4
4
 
5
+ // Re-export pure utils for backwards compatibility
6
+ export {
7
+ isValidMarkdownTemplatePosition,
8
+ calculateMarkdownIndentation,
9
+ } from "./markdown-utils";
10
+
5
11
  // Decoration marks for Markdown syntax highlighting using inline styles
6
12
  const mdHeadingMark = Decoration.mark({
7
13
  attributes: { style: "color: hsl(280, 65%, 60%); font-weight: bold" },
@@ -25,36 +31,6 @@ const templateMark = Decoration.mark({
25
31
  attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
26
32
  });
27
33
 
28
- /**
29
- * Check if cursor is in a valid Markdown template position.
30
- * Templates can appear almost anywhere in Markdown (text content).
31
- * @internal Exported for testing
32
- */
33
- export function isValidMarkdownTemplatePosition(text: string): boolean {
34
- // In Markdown, templates are valid almost everywhere except:
35
- // - Inside code blocks (``` ... ```)
36
- // - Inside inline code (` ... `)
37
-
38
- // Check for unclosed code blocks
39
- const codeBlockMatches = text.match(/```/g) || [];
40
- if (codeBlockMatches.length % 2 === 1) {
41
- return false; // Inside a code block
42
- }
43
-
44
- // Check if we're inside inline code on the current line
45
- const lastNewline = text.lastIndexOf("\n");
46
- const currentLine = text.slice(lastNewline + 1);
47
- const backticks = (currentLine.match(/`/g) || []).length;
48
-
49
- // If odd number of backticks, we're inside inline code
50
- if (backticks % 2 === 1) {
51
- return false;
52
- }
53
-
54
- // Valid in all other positions
55
- return true;
56
- }
57
-
58
34
  /**
59
35
  * Build Markdown + template decorations.
60
36
  */
@@ -141,36 +117,11 @@ function buildMarkdownDecorations(doc: string): DecorationRange[] {
141
117
  return filtered;
142
118
  }
143
119
 
144
- /**
145
- * Calculate indentation for Markdown.
146
- * Markdown uses list indentation.
147
- * @internal Exported for testing
148
- */
149
- export function calculateMarkdownIndentation(
150
- textBefore: string,
151
- _indentUnit: number,
152
- ): number {
153
- const lines = textBefore.split("\n");
154
- if (lines.length === 0) return 0;
155
-
156
- // Find the last non-empty line
157
- for (let i = lines.length - 1; i >= 0; i--) {
158
- const line = lines[i];
159
- if (line.trim().length === 0) continue;
160
-
161
- // Count leading spaces
162
- const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
163
-
164
- // If line is a list item, next line should be indented if continuing
165
- if (/^\s*[-*+]\s/.test(line) || /^\s*\d+\.\s/.test(line)) {
166
- return leadingSpaces;
167
- }
168
-
169
- return leadingSpaces;
170
- }
171
-
172
- return 0;
173
- }
120
+ // Import the pure utils for use in the language support object
121
+ import {
122
+ isValidMarkdownTemplatePosition,
123
+ calculateMarkdownIndentation,
124
+ } from "./markdown-utils";
174
125
 
175
126
  /**
176
127
  * Markdown language support for CodeEditor with template expression handling.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Pure utility functions for XML 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 XML template position.
8
+ * Templates can appear in:
9
+ * 1. Text content between tags: <tag>|</tag>
10
+ * 2. Attribute values: <tag attr="|">
11
+ * @internal Exported for testing
12
+ */
13
+ export function isValidXmlTemplatePosition(text: string): boolean {
14
+ // Check if we're inside an attribute value (inside quotes after =)
15
+ let inTag = false;
16
+ let inAttrValue = false;
17
+ let quoteChar = "";
18
+
19
+ for (let i = 0; i < text.length; i++) {
20
+ const char = text[i];
21
+
22
+ if (!inTag && char === "<" && text[i + 1] !== "/") {
23
+ inTag = true;
24
+ inAttrValue = false;
25
+ } else if (inTag && char === ">") {
26
+ inTag = false;
27
+ inAttrValue = false;
28
+ } else if (inTag && !inAttrValue && char === "=") {
29
+ // Next should be a quote
30
+ const nextChar = text[i + 1];
31
+ if (nextChar === '"' || nextChar === "'") {
32
+ quoteChar = nextChar;
33
+ inAttrValue = true;
34
+ i++; // Skip the quote
35
+ }
36
+ } else if (inAttrValue && char === quoteChar) {
37
+ inAttrValue = false;
38
+ quoteChar = "";
39
+ }
40
+ }
41
+
42
+ // Valid if inside attribute value
43
+ if (inAttrValue) {
44
+ return true;
45
+ }
46
+
47
+ // Valid if between tags (text content position)
48
+ // Check if we're after a closing > and not inside an opening <
49
+ const lastOpenTag = text.lastIndexOf("<");
50
+ const lastCloseTag = text.lastIndexOf(">");
51
+
52
+ // If last > is after last <, we're in text content
53
+ if (lastCloseTag > lastOpenTag) {
54
+ return true;
55
+ }
56
+
57
+ return false;
58
+ }
59
+
60
+ /**
61
+ * Calculate indentation for XML, properly handling template expressions.
62
+ * @internal Exported for testing
63
+ */
64
+ export function calculateXmlIndentation(
65
+ textBefore: string,
66
+ indentUnit: number,
67
+ ): number {
68
+ let depth = 0;
69
+
70
+ // Match all opening and closing tags
71
+ const tagRegex = /<\/?[\w:-]+[^>]*\/?>/g;
72
+ let match;
73
+
74
+ while ((match = tagRegex.exec(textBefore)) !== null) {
75
+ const tag = match[0];
76
+
77
+ // Self-closing tag: <tag />
78
+ if (tag.endsWith("/>")) {
79
+ // No depth change
80
+ continue;
81
+ }
82
+
83
+ // Closing tag: </tag>
84
+ if (tag.startsWith("</")) {
85
+ depth = Math.max(0, depth - 1);
86
+ continue;
87
+ }
88
+
89
+ // Opening tag: <tag> or <tag attr="value">
90
+ depth++;
91
+ }
92
+
93
+ return depth * indentUnit;
94
+ }
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
- import { isValidXmlTemplatePosition, calculateXmlIndentation } from "./xml";
3
+ import {
4
+ isValidXmlTemplatePosition,
5
+ calculateXmlIndentation,
6
+ } from "./xml-utils";
4
7
 
5
8
  describe("isValidXmlTemplatePosition", () => {
6
9
  describe("text content positions", () => {
@@ -2,6 +2,12 @@ import { xml } from "@codemirror/lang-xml";
2
2
  import { Decoration } from "@codemirror/view";
3
3
  import type { LanguageSupport, DecorationRange } from "./types";
4
4
 
5
+ // Re-export pure utils for backwards compatibility
6
+ export {
7
+ isValidXmlTemplatePosition,
8
+ calculateXmlIndentation,
9
+ } from "./xml-utils";
10
+
5
11
  // Decoration marks for XML syntax highlighting using inline styles
6
12
  const xmlTagMark = Decoration.mark({
7
13
  attributes: { style: "color: hsl(280, 65%, 60%)" },
@@ -16,60 +22,6 @@ const templateMark = Decoration.mark({
16
22
  attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
17
23
  });
18
24
 
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
25
  /**
74
26
  * Build XML + template decorations.
75
27
  */
@@ -147,41 +99,11 @@ function buildXmlDecorations(doc: string): DecorationRange[] {
147
99
  return filtered;
148
100
  }
149
101
 
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
- }
102
+ // Import the pure utils for use in the language support object
103
+ import {
104
+ isValidXmlTemplatePosition,
105
+ calculateXmlIndentation,
106
+ } from "./xml-utils";
185
107
 
186
108
  /**
187
109
  * XML language support for CodeEditor with template expression handling.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Pure utility functions for YAML 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 YAML template position.
8
+ * In YAML, templates can appear as values after "key: ".
9
+ * @internal Exported for testing
10
+ */
11
+ export function isValidYamlTemplatePosition(text: string): boolean {
12
+ // Find the current line
13
+ const lastNewline = text.lastIndexOf("\n");
14
+ const currentLine = text.slice(lastNewline + 1);
15
+
16
+ // Valid positions in YAML:
17
+ // 1. After a colon and space (value position): "key: |"
18
+ // 2. Inside a quoted string: 'key: "hello|'
19
+ // 3. In a list item position: "- |"
20
+
21
+ // Check if we're inside a quoted string
22
+ const singleQuotes = (currentLine.match(/'/g) || []).length;
23
+ const doubleQuotes = (currentLine.match(/"/g) || []).length;
24
+ if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1) {
25
+ return true;
26
+ }
27
+
28
+ // Check if we're after "key: " (value position)
29
+ const colonMatch = currentLine.match(/^\s*[\w-]+:\s+/);
30
+ if (colonMatch) {
31
+ return true;
32
+ }
33
+
34
+ // Check if we're in a list item position "- "
35
+ const listMatch = currentLine.match(/^\s*-\s+/);
36
+ if (listMatch) {
37
+ return true;
38
+ }
39
+
40
+ // Check if we're after ": " anywhere in the line
41
+ if (currentLine.includes(": ")) {
42
+ const afterColon = currentLine.split(": ").slice(1).join(": ");
43
+ // If we're in the value part (after colon)
44
+ if (afterColon.length > 0 || currentLine.endsWith(": ")) {
45
+ return true;
46
+ }
47
+ }
48
+
49
+ return false;
50
+ }
51
+
52
+ /**
53
+ * Calculate indentation for YAML, properly handling template expressions.
54
+ * YAML uses indentation-based structure.
55
+ * @internal Exported for testing
56
+ */
57
+ export function calculateYamlIndentation(
58
+ textBefore: string,
59
+ indentUnit: number,
60
+ ): number {
61
+ const lines = textBefore.split("\n");
62
+ if (lines.length === 0) return 0;
63
+
64
+ // Get the last line (the one we're currently on after Enter)
65
+ const currentLine = lines.at(-1);
66
+
67
+ // If current line is empty or whitespace, look at previous line
68
+ if (currentLine?.trim().length === 0 && lines.length > 1) {
69
+ // Find the last non-empty line
70
+ for (let i = lines.length - 2; i >= 0; i--) {
71
+ const line = lines[i];
72
+ if (line.trim().length === 0) continue;
73
+
74
+ // Count leading spaces
75
+ const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
76
+
77
+ // If line ends with ":" it's a key that starts a new block
78
+ if (line.trimEnd().endsWith(":")) {
79
+ return leadingSpaces + indentUnit;
80
+ }
81
+
82
+ // If line starts with "- " it's a list item
83
+ if (line.trim().startsWith("-")) {
84
+ return leadingSpaces + indentUnit;
85
+ }
86
+
87
+ // Otherwise, maintain current indentation
88
+ return leadingSpaces;
89
+ }
90
+ }
91
+
92
+ // For non-empty current line, find previous non-empty for context
93
+ for (let i = lines.length - 1; i >= 0; i--) {
94
+ const line = lines[i];
95
+ if (line.trim().length === 0) continue;
96
+ const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
97
+ return leadingSpaces;
98
+ }
99
+
100
+ return 0;
101
+ }
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
- import { isValidYamlTemplatePosition, calculateYamlIndentation } from "./yaml";
3
+ import {
4
+ isValidYamlTemplatePosition,
5
+ calculateYamlIndentation,
6
+ } from "./yaml-utils";
4
7
 
5
8
  describe("isValidYamlTemplatePosition", () => {
6
9
  describe("value positions", () => {
@@ -2,6 +2,12 @@ import { yaml } from "@codemirror/lang-yaml";
2
2
  import { Decoration } from "@codemirror/view";
3
3
  import type { LanguageSupport, DecorationRange } from "./types";
4
4
 
5
+ // Re-export pure utils for backwards compatibility
6
+ export {
7
+ isValidYamlTemplatePosition,
8
+ calculateYamlIndentation,
9
+ } from "./yaml-utils";
10
+
5
11
  // Decoration marks for YAML syntax highlighting using inline styles
6
12
  const yamlKeyMark = Decoration.mark({
7
13
  attributes: { style: "color: hsl(210, 100%, 75%)" }, // Bright blue for better visibility
@@ -19,52 +25,6 @@ const templateMark = Decoration.mark({
19
25
  attributes: { style: "color: hsl(190, 70%, 50%); font-weight: 500" },
20
26
  });
21
27
 
22
- /**
23
- * Check if cursor is in a valid YAML template position.
24
- * In YAML, templates can appear as values after "key: ".
25
- * @internal Exported for testing
26
- */
27
- export function isValidYamlTemplatePosition(text: string): boolean {
28
- // Find the current line
29
- const lastNewline = text.lastIndexOf("\n");
30
- const currentLine = text.slice(lastNewline + 1);
31
-
32
- // Valid positions in YAML:
33
- // 1. After a colon and space (value position): "key: |"
34
- // 2. Inside a quoted string: 'key: "hello|'
35
- // 3. In a list item position: "- |"
36
-
37
- // Check if we're inside a quoted string
38
- const singleQuotes = (currentLine.match(/'/g) || []).length;
39
- const doubleQuotes = (currentLine.match(/"/g) || []).length;
40
- if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1) {
41
- return true;
42
- }
43
-
44
- // Check if we're after "key: " (value position)
45
- const colonMatch = currentLine.match(/^\s*[\w-]+:\s+/);
46
- if (colonMatch) {
47
- return true;
48
- }
49
-
50
- // Check if we're in a list item position "- "
51
- const listMatch = currentLine.match(/^\s*-\s+/);
52
- if (listMatch) {
53
- return true;
54
- }
55
-
56
- // Check if we're after ": " anywhere in the line
57
- if (currentLine.includes(": ")) {
58
- const afterColon = currentLine.split(": ").slice(1).join(": ");
59
- // If we're in the value part (after colon)
60
- if (afterColon.length > 0 || currentLine.endsWith(": ")) {
61
- return true;
62
- }
63
- }
64
-
65
- return false;
66
- }
67
-
68
28
  /**
69
29
  * Build YAML + template decorations.
70
30
  */
@@ -143,56 +103,11 @@ function buildYamlDecorations(doc: string): DecorationRange[] {
143
103
  return filtered;
144
104
  }
145
105
 
146
- /**
147
- * Calculate indentation for YAML, properly handling template expressions.
148
- * YAML uses indentation-based structure.
149
- * @internal Exported for testing
150
- */
151
- export function calculateYamlIndentation(
152
- textBefore: string,
153
- indentUnit: number,
154
- ): number {
155
- const lines = textBefore.split("\n");
156
- if (lines.length === 0) return 0;
157
-
158
- // Get the last line (the one we're currently on after Enter)
159
- const currentLine = lines.at(-1);
160
-
161
- // If current line is empty or whitespace, look at previous line
162
- if (currentLine?.trim().length === 0 && lines.length > 1) {
163
- // Find the last non-empty line
164
- for (let i = lines.length - 2; i >= 0; i--) {
165
- const line = lines[i];
166
- if (line.trim().length === 0) continue;
167
-
168
- // Count leading spaces
169
- const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
170
-
171
- // If line ends with ":" it's a key that starts a new block
172
- if (line.trimEnd().endsWith(":")) {
173
- return leadingSpaces + indentUnit;
174
- }
175
-
176
- // If line starts with "- " it's a list item
177
- if (line.trim().startsWith("-")) {
178
- return leadingSpaces + indentUnit;
179
- }
180
-
181
- // Otherwise, maintain current indentation
182
- return leadingSpaces;
183
- }
184
- }
185
-
186
- // For non-empty current line, find previous non-empty for context
187
- for (let i = lines.length - 1; i >= 0; i--) {
188
- const line = lines[i];
189
- if (line.trim().length === 0) continue;
190
- const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
191
- return leadingSpaces;
192
- }
193
-
194
- return 0;
195
- }
106
+ // Import the pure utils for use in the language support object
107
+ import {
108
+ isValidYamlTemplatePosition,
109
+ calculateYamlIndentation,
110
+ } from "./yaml-utils";
196
111
 
197
112
  /**
198
113
  * YAML language support for CodeEditor with template expression handling.