@checkstack/ui 0.3.0 → 0.4.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 +38 -0
- package/package.json +3 -1
- package/src/components/CodeEditor/languageSupport/json-utils.ts +117 -0
- package/src/components/CodeEditor/languageSupport/json.test.ts +4 -1
- package/src/components/CodeEditor/languageSupport/json.ts +11 -112
- package/src/components/CodeEditor/languageSupport/markdown-utils.ts +65 -0
- package/src/components/CodeEditor/languageSupport/markdown.test.ts +1 -1
- package/src/components/CodeEditor/languageSupport/markdown.ts +11 -60
- package/src/components/CodeEditor/languageSupport/xml-utils.ts +94 -0
- package/src/components/CodeEditor/languageSupport/xml.test.ts +4 -1
- package/src/components/CodeEditor/languageSupport/xml.ts +11 -89
- package/src/components/CodeEditor/languageSupport/yaml-utils.ts +101 -0
- package/src/components/CodeEditor/languageSupport/yaml.test.ts +4 -1
- package/src/components/CodeEditor/languageSupport/yaml.ts +11 -96
- package/src/components/DateRangeFilter.tsx +12 -4
- package/src/components/DateTimePicker.tsx +332 -39
- package/src/components/Dialog.tsx +10 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @checkstack/ui
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d1324e6: Enhanced DateTimePicker with calendar popup and independent field editing
|
|
8
|
+
|
|
9
|
+
- Added calendar popup using `react-day-picker` and Radix Popover for date selection
|
|
10
|
+
- Implemented independent input fields for day, month, year, hour, and minute
|
|
11
|
+
- Added input validation with proper clamping on blur (respects leap years)
|
|
12
|
+
- Updated `onChange` signature to `Date | undefined` to handle invalid states
|
|
13
|
+
- Fixed Dialog focus ring clipping by adding wrapper with negative margin/padding
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 2c0822d: ### Queue System
|
|
18
|
+
|
|
19
|
+
- Added cron pattern support to `scheduleRecurring()` - accepts either `intervalSeconds` or `cronPattern`
|
|
20
|
+
- BullMQ backend uses native cron scheduling via `pattern` option
|
|
21
|
+
- InMemoryQueue implements wall-clock cron scheduling with `cron-parser`
|
|
22
|
+
|
|
23
|
+
### Maintenance Backend
|
|
24
|
+
|
|
25
|
+
- Auto status transitions now use cron pattern `* * * * *` for precise second-0 scheduling
|
|
26
|
+
- User notifications are now sent for auto-started and auto-completed maintenances
|
|
27
|
+
- Refactored to call `addUpdate` RPC for status changes, centralizing hook/signal/notification logic
|
|
28
|
+
|
|
29
|
+
### UI
|
|
30
|
+
|
|
31
|
+
- DateTimePicker now resets seconds and milliseconds to 0 when time is changed
|
|
32
|
+
|
|
33
|
+
## 0.3.1
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- Updated dependencies [8a87cd4]
|
|
38
|
+
- @checkstack/common@0.5.0
|
|
39
|
+
- @checkstack/frontend-api@0.3.2
|
|
40
|
+
|
|
3
41
|
## 0.3.0
|
|
4
42
|
|
|
5
43
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"@codemirror/view": "^6.39.11",
|
|
17
17
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
18
18
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
19
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
19
20
|
"@radix-ui/react-select": "^2.2.6",
|
|
20
21
|
"@radix-ui/react-slot": "^1.2.4",
|
|
21
22
|
"@uiw/react-codemirror": "^4.25.4",
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"date-fns": "^4.1.0",
|
|
27
28
|
"lucide-react": "0.562.0",
|
|
28
29
|
"react": "^18.2.0",
|
|
30
|
+
"react-day-picker": "^9.13.0",
|
|
29
31
|
"react-markdown": "^10.1.0",
|
|
30
32
|
"react-router-dom": "^6.20.0",
|
|
31
33
|
"recharts": "^3.6.0",
|
|
@@ -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 {
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 {
|
|
3
|
+
import {
|
|
4
|
+
isValidXmlTemplatePosition,
|
|
5
|
+
calculateXmlIndentation,
|
|
6
|
+
} from "./xml-utils";
|
|
4
7
|
|
|
5
8
|
describe("isValidXmlTemplatePosition", () => {
|
|
6
9
|
describe("text content positions", () => {
|