@fc-components/monaco-editor 0.2.1 → 0.3.2
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/dist/expr/__tests__/__mocks__/monaco-editor.d.ts +28 -0
- package/dist/expr/completion/getCompletionProvider.d.ts +4 -0
- package/dist/expr/expr.d.ts +83 -0
- package/dist/expr/index.d.ts +3 -0
- package/dist/expr/parser/index.d.ts +3 -0
- package/dist/expr/parser/lexer.d.ts +27 -0
- package/dist/expr/parser/parser.d.ts +66 -0
- package/dist/expr/parser/types.d.ts +32 -0
- package/dist/expr/types.d.ts +17 -0
- package/dist/expr/validation.d.ts +12 -0
- package/dist/index.d.ts +2 -0
- package/dist/monaco-editor.cjs.development.js +1987 -3
- package/dist/monaco-editor.cjs.development.js.map +1 -1
- package/dist/monaco-editor.cjs.production.min.js +1 -1
- package/dist/monaco-editor.cjs.production.min.js.map +1 -1
- package/dist/monaco-editor.esm.js +1991 -8
- package/dist/monaco-editor.esm.js.map +1 -1
- package/dist/sql/format.d.ts +1 -0
- package/dist/sql/index.d.ts +9 -2
- package/package.json +6 -1
- package/src/expr/__tests__/__mocks__/monaco-editor.ts +34 -0
- package/src/expr/__tests__/expr.test.tsx +339 -0
- package/src/expr/completion/getCompletionProvider.ts +133 -0
- package/src/expr/expr.ts +229 -0
- package/src/expr/index.tsx +322 -0
- package/src/expr/parser/index.ts +3 -0
- package/src/expr/parser/lexer.ts +377 -0
- package/src/expr/parser/parser.ts +581 -0
- package/src/expr/parser/types.ts +77 -0
- package/src/expr/types.ts +17 -0
- package/src/expr/validation.ts +209 -0
- package/src/index.tsx +2 -0
- package/src/sql/format.ts +13 -0
- package/src/sql/index.tsx +91 -3
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import * as monaco from 'monaco-editor';
|
|
2
|
+
import { ExprParser } from './parser';
|
|
3
|
+
|
|
4
|
+
const EXPR_LANG_ID = 'expr';
|
|
5
|
+
|
|
6
|
+
const parser = new ExprParser();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate an expr-lang expression and return Monaco editor markers.
|
|
10
|
+
* Uses the recursive descent parser for syntax errors,
|
|
11
|
+
* plus regex-based checks for structural issues like unbalanced quotes/brackets.
|
|
12
|
+
*/
|
|
13
|
+
export const validateExpr = (expr: string): Omit<monaco.editor.IMarker, 'owner' | 'resource'>[] => {
|
|
14
|
+
const markers: Omit<monaco.editor.IMarker, 'owner' | 'resource'>[] = [];
|
|
15
|
+
|
|
16
|
+
if (!expr || expr.trim().length === 0) {
|
|
17
|
+
return markers;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 1. Parser-based syntax validation
|
|
21
|
+
const parseErrors = parser.parse(expr);
|
|
22
|
+
for (const err of parseErrors) {
|
|
23
|
+
markers.push({
|
|
24
|
+
severity: monaco.MarkerSeverity.Error,
|
|
25
|
+
startLineNumber: err.startLine,
|
|
26
|
+
startColumn: err.startColumn,
|
|
27
|
+
endLineNumber: err.endLine,
|
|
28
|
+
endColumn: err.endColumn,
|
|
29
|
+
message: err.message,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Line-level structural checks (fallback for issues the parser might miss)
|
|
34
|
+
const structuralMarkers = validateLineLevel(expr);
|
|
35
|
+
markers.push(...structuralMarkers);
|
|
36
|
+
|
|
37
|
+
return markers;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Basic line-level validation for quotes and bracket balancing.
|
|
42
|
+
*/
|
|
43
|
+
function validateLineLevel(expr: string): Omit<monaco.editor.IMarker, 'owner' | 'resource'>[] {
|
|
44
|
+
const markers: Omit<monaco.editor.IMarker, 'owner' | 'resource'>[] = [];
|
|
45
|
+
const lines = expr.split('\n');
|
|
46
|
+
let inBlockComment = false;
|
|
47
|
+
|
|
48
|
+
lines.forEach((line, index) => {
|
|
49
|
+
const lineNumber = index + 1;
|
|
50
|
+
|
|
51
|
+
// Track block comment state
|
|
52
|
+
if (!inBlockComment) {
|
|
53
|
+
const blockCommentStart = line.indexOf('/*');
|
|
54
|
+
if (blockCommentStart !== -1) {
|
|
55
|
+
const blockCommentEnd = line.indexOf('*/', blockCommentStart + 2);
|
|
56
|
+
if (blockCommentEnd === -1) {
|
|
57
|
+
inBlockComment = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
const blockCommentEnd = line.indexOf('*/');
|
|
62
|
+
if (blockCommentEnd !== -1) {
|
|
63
|
+
inBlockComment = false;
|
|
64
|
+
}
|
|
65
|
+
if (inBlockComment || line.trim().startsWith('/*')) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const trimmedLine = line.trim();
|
|
71
|
+
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('/*')) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for unclosed quotes
|
|
76
|
+
const singleQuotes = countQuotesOutsideBlockComments(line);
|
|
77
|
+
if (singleQuotes % 2 !== 0) {
|
|
78
|
+
markers.push({
|
|
79
|
+
severity: monaco.MarkerSeverity.Warning,
|
|
80
|
+
startLineNumber: lineNumber,
|
|
81
|
+
startColumn: 1,
|
|
82
|
+
endLineNumber: lineNumber,
|
|
83
|
+
endColumn: line.length + 1,
|
|
84
|
+
message: "Unclosed single quote '",
|
|
85
|
+
source: EXPR_LANG_ID,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const doubleQuotes = countDoubleQuotesOutsideBlockComments(line);
|
|
90
|
+
if (doubleQuotes % 2 !== 0) {
|
|
91
|
+
markers.push({
|
|
92
|
+
severity: monaco.MarkerSeverity.Warning,
|
|
93
|
+
startLineNumber: lineNumber,
|
|
94
|
+
startColumn: 1,
|
|
95
|
+
endLineNumber: lineNumber,
|
|
96
|
+
endColumn: line.length + 1,
|
|
97
|
+
message: 'Unclosed double quote "',
|
|
98
|
+
source: EXPR_LANG_ID,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const backticks = (line.match(/`/g) || []).length;
|
|
103
|
+
if (backticks % 2 !== 0) {
|
|
104
|
+
markers.push({
|
|
105
|
+
severity: monaco.MarkerSeverity.Warning,
|
|
106
|
+
startLineNumber: lineNumber,
|
|
107
|
+
startColumn: 1,
|
|
108
|
+
endLineNumber: lineNumber,
|
|
109
|
+
endColumn: line.length + 1,
|
|
110
|
+
message: 'Unclosed backtick `',
|
|
111
|
+
source: EXPR_LANG_ID,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check for unbalanced brackets
|
|
116
|
+
const openParens = (line.match(/\(/g) || []).length;
|
|
117
|
+
const closeParens = (line.match(/\)/g) || []).length;
|
|
118
|
+
if (openParens > closeParens) {
|
|
119
|
+
markers.push({
|
|
120
|
+
severity: monaco.MarkerSeverity.Warning,
|
|
121
|
+
startLineNumber: lineNumber,
|
|
122
|
+
startColumn: 1,
|
|
123
|
+
endLineNumber: lineNumber,
|
|
124
|
+
endColumn: line.length + 1,
|
|
125
|
+
message: 'Unmatched opening parenthesis',
|
|
126
|
+
source: EXPR_LANG_ID,
|
|
127
|
+
});
|
|
128
|
+
} else if (closeParens > openParens) {
|
|
129
|
+
markers.push({
|
|
130
|
+
severity: monaco.MarkerSeverity.Warning,
|
|
131
|
+
startLineNumber: lineNumber,
|
|
132
|
+
startColumn: 1,
|
|
133
|
+
endLineNumber: lineNumber,
|
|
134
|
+
endColumn: line.length + 1,
|
|
135
|
+
message: 'Unmatched closing parenthesis',
|
|
136
|
+
source: EXPR_LANG_ID,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const openBrackets = (line.match(/\[/g) || []).length;
|
|
141
|
+
const closeBrackets = (line.match(/\]/g) || []).length;
|
|
142
|
+
if (openBrackets > closeBrackets) {
|
|
143
|
+
markers.push({
|
|
144
|
+
severity: monaco.MarkerSeverity.Warning,
|
|
145
|
+
startLineNumber: lineNumber,
|
|
146
|
+
startColumn: 1,
|
|
147
|
+
endLineNumber: lineNumber,
|
|
148
|
+
endColumn: line.length + 1,
|
|
149
|
+
message: 'Unmatched opening bracket',
|
|
150
|
+
source: EXPR_LANG_ID,
|
|
151
|
+
});
|
|
152
|
+
} else if (closeBrackets > openBrackets) {
|
|
153
|
+
markers.push({
|
|
154
|
+
severity: monaco.MarkerSeverity.Warning,
|
|
155
|
+
startLineNumber: lineNumber,
|
|
156
|
+
startColumn: 1,
|
|
157
|
+
endLineNumber: lineNumber,
|
|
158
|
+
endColumn: line.length + 1,
|
|
159
|
+
message: 'Unmatched closing bracket',
|
|
160
|
+
source: EXPR_LANG_ID,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
165
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
166
|
+
if (openBraces > closeBraces) {
|
|
167
|
+
markers.push({
|
|
168
|
+
severity: monaco.MarkerSeverity.Warning,
|
|
169
|
+
startLineNumber: lineNumber,
|
|
170
|
+
startColumn: 1,
|
|
171
|
+
endLineNumber: lineNumber,
|
|
172
|
+
endColumn: line.length + 1,
|
|
173
|
+
message: 'Unmatched opening curly brace',
|
|
174
|
+
source: EXPR_LANG_ID,
|
|
175
|
+
});
|
|
176
|
+
} else if (closeBraces > openBraces) {
|
|
177
|
+
markers.push({
|
|
178
|
+
severity: monaco.MarkerSeverity.Warning,
|
|
179
|
+
startLineNumber: lineNumber,
|
|
180
|
+
startColumn: 1,
|
|
181
|
+
endLineNumber: lineNumber,
|
|
182
|
+
endColumn: line.length + 1,
|
|
183
|
+
message: 'Unmatched closing curly brace',
|
|
184
|
+
source: EXPR_LANG_ID,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return markers;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function countQuotesOutsideBlockComments(line: string): number {
|
|
193
|
+
const withoutBlockComments = line.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
194
|
+
return (withoutBlockComments.match(/'/g) || []).length;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function countDoubleQuotesOutsideBlockComments(line: string): number {
|
|
198
|
+
const withoutBlockComments = line.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
199
|
+
return (withoutBlockComments.match(/"/g) || []).length;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Validate an expr-lang expression and return a plain array of human-readable error messages.
|
|
204
|
+
* Returns an empty array if the expression is valid.
|
|
205
|
+
*/
|
|
206
|
+
export function checkExpr(expr: string): string[] {
|
|
207
|
+
const errors = parser.parse(expr);
|
|
208
|
+
return errors.map((e) => `${e.startLine}:${e.startColumn}: ${e.message}`);
|
|
209
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import promql from './promql';
|
|
2
2
|
import yaml from './yaml';
|
|
3
3
|
import sql from './sql';
|
|
4
|
+
import expr from './expr';
|
|
4
5
|
|
|
5
6
|
export { promql as PromQLMonacoEditor };
|
|
6
7
|
export { yaml as YamlMonacoEditor };
|
|
7
8
|
export { sql as SqlMonacoEditor };
|
|
9
|
+
export { expr as ExprMonacoEditor };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { format } from 'sql-formatter';
|
|
2
|
+
|
|
3
|
+
export function formatSql(sql: string): string {
|
|
4
|
+
try {
|
|
5
|
+
return format(sql, {
|
|
6
|
+
tabWidth: 2,
|
|
7
|
+
keywordCase: 'upper',
|
|
8
|
+
});
|
|
9
|
+
} catch {
|
|
10
|
+
// If formatting fails, return the original SQL
|
|
11
|
+
return sql;
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/sql/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useRef } from 'react';
|
|
1
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
|
2
2
|
import MonacoEditor from 'react-monaco-editor';
|
|
3
3
|
import * as monaco from 'monaco-editor';
|
|
4
4
|
import type * as monacoTypes from 'monaco-editor/esm/vs/editor/editor.api';
|
|
@@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
6
6
|
import { css } from '@emotion/css';
|
|
7
7
|
import { language, languageConfiguration } from './sql';
|
|
8
8
|
import { getSqlCompletionProvider } from './completion/getCompletionProvider';
|
|
9
|
+
import { formatSql } from './format';
|
|
9
10
|
|
|
10
11
|
interface SqlEditorProps {
|
|
11
12
|
className?: string;
|
|
@@ -23,6 +24,14 @@ interface SqlEditorProps {
|
|
|
23
24
|
onBlur?: (value: string) => void;
|
|
24
25
|
onFocus?: (value: string) => void;
|
|
25
26
|
editorDidMount?: (editor: monacoTypes.editor.IStandaloneCodeEditor) => void;
|
|
27
|
+
/** Whether to show the format button. Default: false */
|
|
28
|
+
enableFormat?: boolean;
|
|
29
|
+
/** Custom format button renderer */
|
|
30
|
+
renderFormatButton?: (handleFormat: () => void) => React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SqlEditorHandle {
|
|
34
|
+
format: () => void;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
const SQL_LANG_ID = 'sql';
|
|
@@ -73,7 +82,32 @@ const containerReadOnlyClassName = css`
|
|
|
73
82
|
}
|
|
74
83
|
`;
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
const formatBtnClassName = css`
|
|
86
|
+
position: absolute;
|
|
87
|
+
top: 4px;
|
|
88
|
+
right: 4px;
|
|
89
|
+
z-index: 10;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
width: 24px;
|
|
94
|
+
height: 24px;
|
|
95
|
+
padding: 0;
|
|
96
|
+
border: none;
|
|
97
|
+
background: transparent;
|
|
98
|
+
border-radius: 4px;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
color: inherit;
|
|
101
|
+
opacity: 0.6;
|
|
102
|
+
transition: opacity 0.15s;
|
|
103
|
+
|
|
104
|
+
&:hover {
|
|
105
|
+
opacity: 1;
|
|
106
|
+
background: var(--format-btn-hover-bg, rgba(128, 128, 128, 0.15));
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const SqlEditor = forwardRef<SqlEditorHandle, SqlEditorProps>((props, ref) => {
|
|
77
111
|
const id = uuidv4();
|
|
78
112
|
const {
|
|
79
113
|
className,
|
|
@@ -83,6 +117,7 @@ export default function SqlEditor(props: SqlEditorProps) {
|
|
|
83
117
|
theme = 'light',
|
|
84
118
|
value = '',
|
|
85
119
|
placeholder,
|
|
120
|
+
enableFormat = false,
|
|
86
121
|
enableAutocomplete = true,
|
|
87
122
|
readOnly = false,
|
|
88
123
|
disabled = false,
|
|
@@ -91,6 +126,7 @@ export default function SqlEditor(props: SqlEditorProps) {
|
|
|
91
126
|
onBlur,
|
|
92
127
|
onFocus,
|
|
93
128
|
editorDidMount,
|
|
129
|
+
renderFormatButton,
|
|
94
130
|
} = props;
|
|
95
131
|
|
|
96
132
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -223,6 +259,18 @@ export default function SqlEditor(props: SqlEditorProps) {
|
|
|
223
259
|
editorDidMount?.(editor);
|
|
224
260
|
};
|
|
225
261
|
|
|
262
|
+
const handleFormat = useCallback(() => {
|
|
263
|
+
const editor = editorRef.current;
|
|
264
|
+
if (!editor) return;
|
|
265
|
+
const currentValue = editor.getValue();
|
|
266
|
+
const formatted = formatSql(currentValue);
|
|
267
|
+
editor.setValue(formatted);
|
|
268
|
+
}, []);
|
|
269
|
+
|
|
270
|
+
useImperativeHandle(ref, () => ({
|
|
271
|
+
format: handleFormat,
|
|
272
|
+
}));
|
|
273
|
+
|
|
226
274
|
const themeValue = themeMap[theme];
|
|
227
275
|
|
|
228
276
|
return (
|
|
@@ -238,6 +286,7 @@ export default function SqlEditor(props: SqlEditorProps) {
|
|
|
238
286
|
display: 'block',
|
|
239
287
|
resize: 'vertical',
|
|
240
288
|
overflow: 'auto',
|
|
289
|
+
position: 'relative',
|
|
241
290
|
minHeight: SIZE_MAP[size].minHeight,
|
|
242
291
|
maxHeight,
|
|
243
292
|
}}
|
|
@@ -285,6 +334,45 @@ export default function SqlEditor(props: SqlEditorProps) {
|
|
|
285
334
|
}}
|
|
286
335
|
/>
|
|
287
336
|
</div>
|
|
337
|
+
{enableFormat &&
|
|
338
|
+
(renderFormatButton ? (
|
|
339
|
+
renderFormatButton(handleFormat)
|
|
340
|
+
) : (
|
|
341
|
+
<button
|
|
342
|
+
onClick={handleFormat}
|
|
343
|
+
className={formatBtnClassName}
|
|
344
|
+
title='Format SQL'
|
|
345
|
+
style={
|
|
346
|
+
{
|
|
347
|
+
'--format-btn-hover-bg':
|
|
348
|
+
theme === 'dark' ? 'rgba(255,255,255,0.2)' : 'rgba(128,128,128,0.15)',
|
|
349
|
+
} as React.CSSProperties
|
|
350
|
+
}
|
|
351
|
+
>
|
|
352
|
+
<svg
|
|
353
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
354
|
+
width='16'
|
|
355
|
+
height='16'
|
|
356
|
+
viewBox='0 0 24 24'
|
|
357
|
+
fill='none'
|
|
358
|
+
stroke='currentColor'
|
|
359
|
+
strokeWidth='2'
|
|
360
|
+
strokeLinecap='round'
|
|
361
|
+
strokeLinejoin='round'
|
|
362
|
+
>
|
|
363
|
+
<path d='m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72' />
|
|
364
|
+
<path d='m14 7 3 3' />
|
|
365
|
+
<path d='M5 6v4' />
|
|
366
|
+
<path d='M19 14v4' />
|
|
367
|
+
<path d='M10 2v2' />
|
|
368
|
+
<path d='M7 8H3' />
|
|
369
|
+
<path d='M21 16h-4' />
|
|
370
|
+
<path d='M11 3H9' />
|
|
371
|
+
</svg>
|
|
372
|
+
</button>
|
|
373
|
+
))}
|
|
288
374
|
</div>
|
|
289
375
|
);
|
|
290
|
-
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
export default SqlEditor;
|