@fc-components/monaco-editor 0.1.12 → 0.1.14
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/index.d.ts +2 -0
- package/dist/monaco-editor.cjs.development.js +923 -1
- 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 +923 -2
- package/dist/monaco-editor.esm.js.map +1 -1
- package/dist/yaml/completion/getCompletionProvider.d.ts +4 -0
- package/dist/yaml/index.d.ts +19 -0
- package/dist/yaml/types.d.ts +39 -0
- package/dist/yaml/validation.d.ts +3 -0
- package/dist/yaml/yaml.d.ts +56 -0
- package/package.json +1 -1
- package/src/index.tsx +2 -0
- package/src/promql/index.tsx +4 -0
- package/src/yaml/completion/getCompletionProvider.ts +431 -0
- package/src/yaml/index.tsx +385 -0
- package/src/yaml/types.ts +44 -0
- package/src/yaml/validation.ts +127 -0
- package/src/yaml/yaml.ts +150 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import MonacoEditor from 'react-monaco-editor';
|
|
3
|
+
import * as monaco from 'monaco-editor';
|
|
4
|
+
import type * as monacoTypes from 'monaco-editor/esm/vs/editor/editor.api';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { css } from '@emotion/css';
|
|
7
|
+
|
|
8
|
+
import { language, languageConfiguration } from './yaml';
|
|
9
|
+
import { getYamlCompletionProvider } from './completion/getCompletionProvider';
|
|
10
|
+
import { validateYaml } from './validation';
|
|
11
|
+
import type { YamlEditorSchema } from './types';
|
|
12
|
+
|
|
13
|
+
interface YamlEditorProps {
|
|
14
|
+
size?: 'small' | 'middle' | 'large';
|
|
15
|
+
theme?: 'light' | 'dark';
|
|
16
|
+
value?: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
enableAutocomplete?: boolean;
|
|
19
|
+
readOnly?: boolean;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
schemas?: YamlEditorSchema[];
|
|
22
|
+
onChange?: (value: string) => void;
|
|
23
|
+
onEnter?: (value: string) => void;
|
|
24
|
+
onBlur?: (value: string) => void;
|
|
25
|
+
editorDidMount?: (editor: monacoTypes.editor.IStandaloneCodeEditor) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const YAML_LANG_ID = 'yaml';
|
|
29
|
+
const SIZE_MAP: Record<
|
|
30
|
+
string,
|
|
31
|
+
{
|
|
32
|
+
className: string;
|
|
33
|
+
top: number;
|
|
34
|
+
bottom: number;
|
|
35
|
+
}
|
|
36
|
+
> = {
|
|
37
|
+
small: {
|
|
38
|
+
className: 'ant-input-sm',
|
|
39
|
+
top: 1,
|
|
40
|
+
bottom: 1,
|
|
41
|
+
},
|
|
42
|
+
middle: {
|
|
43
|
+
className: 'ant-input-md',
|
|
44
|
+
top: 1,
|
|
45
|
+
bottom: 1,
|
|
46
|
+
},
|
|
47
|
+
large: {
|
|
48
|
+
className: 'ant-input-lg',
|
|
49
|
+
top: 3,
|
|
50
|
+
bottom: 2,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const themeMap: Record<string, string> = {
|
|
55
|
+
light: 'yaml-light',
|
|
56
|
+
dark: 'yaml-dark',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const containerDisabledClassName = css`
|
|
60
|
+
.monaco-editor {
|
|
61
|
+
user-select: none;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const containerReadOnlyClassName = css`
|
|
67
|
+
.monaco-editor .cursors-layer > .cursor {
|
|
68
|
+
opacity: 0 !important;
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const getStyles = (placeholder?: string) => {
|
|
73
|
+
return {
|
|
74
|
+
placeholder: css({
|
|
75
|
+
'::after': {
|
|
76
|
+
content: `'${placeholder}'`,
|
|
77
|
+
opacity: 0.6,
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default function YamlEditor(props: YamlEditorProps) {
|
|
84
|
+
const id = uuidv4();
|
|
85
|
+
const {
|
|
86
|
+
size = 'middle',
|
|
87
|
+
theme = 'light',
|
|
88
|
+
value,
|
|
89
|
+
placeholder,
|
|
90
|
+
enableAutocomplete = true,
|
|
91
|
+
readOnly = false,
|
|
92
|
+
disabled = false,
|
|
93
|
+
schemas = [],
|
|
94
|
+
onChange,
|
|
95
|
+
onEnter,
|
|
96
|
+
onBlur,
|
|
97
|
+
editorDidMount,
|
|
98
|
+
} = props;
|
|
99
|
+
|
|
100
|
+
const autocompleteDisposeFun = useRef<(() => void) | null>(null);
|
|
101
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
102
|
+
const editorRef = useRef<monacoTypes.editor.IStandaloneCodeEditor | null>(null);
|
|
103
|
+
const styles = getStyles(placeholder);
|
|
104
|
+
|
|
105
|
+
const handleEditorDidMount = (editor: monacoTypes.editor.IStandaloneCodeEditor) => {
|
|
106
|
+
editorRef.current = editor;
|
|
107
|
+
|
|
108
|
+
// 定义主题
|
|
109
|
+
monaco.editor.defineTheme('yaml-light', {
|
|
110
|
+
base: 'vs',
|
|
111
|
+
inherit: true,
|
|
112
|
+
rules: [
|
|
113
|
+
{ token: 'key', foreground: '0000FF' },
|
|
114
|
+
{ token: 'string', foreground: '008000' },
|
|
115
|
+
{ token: 'number', foreground: '098658' },
|
|
116
|
+
{ token: 'comment', foreground: '008000', fontStyle: 'italic' },
|
|
117
|
+
{ token: 'delimiter', foreground: '000000' },
|
|
118
|
+
{ token: 'tag', foreground: '800080' },
|
|
119
|
+
{ token: 'keyword', foreground: '0000FF', fontStyle: 'bold' },
|
|
120
|
+
],
|
|
121
|
+
colors: {
|
|
122
|
+
'editor.background': '#00000000',
|
|
123
|
+
focusBorder: '#00000000',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
monaco.editor.defineTheme('yaml-dark', {
|
|
128
|
+
base: 'vs-dark',
|
|
129
|
+
inherit: true,
|
|
130
|
+
rules: [
|
|
131
|
+
{ token: 'key', foreground: '9CDCFE' },
|
|
132
|
+
{ token: 'string', foreground: 'CE9178' },
|
|
133
|
+
{ token: 'number', foreground: 'B5CEA8' },
|
|
134
|
+
{ token: 'comment', foreground: '6A9955', fontStyle: 'italic' },
|
|
135
|
+
{ token: 'delimiter', foreground: 'D4D4D4' },
|
|
136
|
+
{ token: 'tag', foreground: 'C586C0' },
|
|
137
|
+
{ token: 'keyword', foreground: '569CD6', fontStyle: 'bold' },
|
|
138
|
+
],
|
|
139
|
+
colors: {
|
|
140
|
+
'editor.background': '#00000000',
|
|
141
|
+
focusBorder: '#00000000',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const isEditorFocused = editor.createContextKey<boolean>('isEditorFocused' + id, false);
|
|
146
|
+
|
|
147
|
+
// 设置焦点状态
|
|
148
|
+
editor.onDidBlurEditorWidget(() => {
|
|
149
|
+
isEditorFocused.set(false);
|
|
150
|
+
onBlur?.(editor.getValue());
|
|
151
|
+
const position = editor.getPosition();
|
|
152
|
+
if (position) {
|
|
153
|
+
const newSelection = new monaco.Selection(position.lineNumber, position.column, position.lineNumber, position.column);
|
|
154
|
+
editor.setSelection(newSelection);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
editor.onDidFocusEditorText(() => {
|
|
159
|
+
isEditorFocused.set(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// 设置编辑器高度自适应
|
|
163
|
+
const updateElementHeight = () => {
|
|
164
|
+
const containerDiv = containerRef.current;
|
|
165
|
+
if (containerDiv !== null) {
|
|
166
|
+
const pixelHeight = Math.max(editor.getContentHeight(), 20);
|
|
167
|
+
containerDiv.style.height = `${pixelHeight}px`;
|
|
168
|
+
containerDiv.style.width = '100%';
|
|
169
|
+
const pixelWidth = containerDiv.clientWidth;
|
|
170
|
+
editor.layout({ width: pixelWidth, height: pixelHeight });
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
editor.onDidContentSizeChange(updateElementHeight);
|
|
175
|
+
updateElementHeight();
|
|
176
|
+
|
|
177
|
+
// 禁用默认的搜索快捷键
|
|
178
|
+
monaco.editor.addKeybindingRule({
|
|
179
|
+
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyF,
|
|
180
|
+
command: null,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Shift + Enter 换行
|
|
184
|
+
editor.addCommand(
|
|
185
|
+
monaco.KeyMod.Shift | monaco.KeyCode.Enter,
|
|
186
|
+
() => {
|
|
187
|
+
const position = editor.getPosition();
|
|
188
|
+
if (position) {
|
|
189
|
+
editor.executeEdits('shift-enter', [
|
|
190
|
+
{
|
|
191
|
+
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
|
|
192
|
+
text: '\n',
|
|
193
|
+
},
|
|
194
|
+
]);
|
|
195
|
+
editor.setPosition({
|
|
196
|
+
lineNumber: position.lineNumber + 1,
|
|
197
|
+
column: 1,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
'isEditorFocused' + id,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Enter 键处理 - 移除阻止默认行为的规则,允许正常换行
|
|
205
|
+
// 只有当提供了 onEnter 回调且没有建议窗口时才触发回调
|
|
206
|
+
if (onEnter) {
|
|
207
|
+
editor.addCommand(
|
|
208
|
+
monaco.KeyCode.Enter,
|
|
209
|
+
() => {
|
|
210
|
+
onEnter(editor.getValue());
|
|
211
|
+
},
|
|
212
|
+
'!suggestWidgetVisible && isEditorFocused' + id,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 内容变化时进行验证
|
|
217
|
+
editor.onDidChangeModelContent(() => {
|
|
218
|
+
const model = editor.getModel();
|
|
219
|
+
if (model) {
|
|
220
|
+
const content = model.getValue();
|
|
221
|
+
const errors = validateYaml(content, schemas);
|
|
222
|
+
const markers = errors.map((error) => ({
|
|
223
|
+
message: error.message,
|
|
224
|
+
severity: error.severity === 'error' ? monaco.MarkerSeverity.Error : error.severity === 'warning' ? monaco.MarkerSeverity.Warning : monaco.MarkerSeverity.Info,
|
|
225
|
+
startLineNumber: error.startLineNumber,
|
|
226
|
+
endLineNumber: error.endLineNumber,
|
|
227
|
+
startColumn: error.startColumn,
|
|
228
|
+
endColumn: error.endColumn,
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
monaco.editor.setModelMarkers(model, 'yaml', markers);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
editorDidMount?.(editor);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
// 注册 YAML 语言
|
|
240
|
+
monaco.languages.register({
|
|
241
|
+
id: YAML_LANG_ID,
|
|
242
|
+
aliases: ['YAML', 'yaml'],
|
|
243
|
+
extensions: ['.yaml', '.yml'],
|
|
244
|
+
mimetypes: ['application/x-yaml', 'text/x-yaml'],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 设置语法高亮
|
|
248
|
+
monaco.languages.setMonarchTokensProvider(YAML_LANG_ID, language as any);
|
|
249
|
+
|
|
250
|
+
// 设置语言配置
|
|
251
|
+
monaco.languages.setLanguageConfiguration(YAML_LANG_ID, languageConfiguration as any);
|
|
252
|
+
|
|
253
|
+
return () => {
|
|
254
|
+
autocompleteDisposeFun.current?.();
|
|
255
|
+
};
|
|
256
|
+
}, []);
|
|
257
|
+
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
const editor = editorRef.current;
|
|
260
|
+
if (!editor) return;
|
|
261
|
+
|
|
262
|
+
// 清理之前的自动补全提供器
|
|
263
|
+
if (autocompleteDisposeFun.current) {
|
|
264
|
+
autocompleteDisposeFun.current();
|
|
265
|
+
autocompleteDisposeFun.current = null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 如果启用自动补全,设置新的补全提供器
|
|
269
|
+
if (enableAutocomplete) {
|
|
270
|
+
const completionProvider = getYamlCompletionProvider(monaco, schemas);
|
|
271
|
+
|
|
272
|
+
const filteringCompletionProvider: monacoTypes.languages.CompletionItemProvider = {
|
|
273
|
+
...completionProvider,
|
|
274
|
+
provideCompletionItems: (model, position, context, token) => {
|
|
275
|
+
if (editor.getModel()?.id !== model.id) {
|
|
276
|
+
return { suggestions: [] };
|
|
277
|
+
}
|
|
278
|
+
return completionProvider.provideCompletionItems(model, position, context, token);
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const { dispose } = monaco.languages.registerCompletionItemProvider(YAML_LANG_ID, filteringCompletionProvider);
|
|
283
|
+
autocompleteDisposeFun.current = dispose;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 处理占位符
|
|
287
|
+
const model = editor.getModel();
|
|
288
|
+
if (model) {
|
|
289
|
+
model.deltaDecorations(
|
|
290
|
+
model.getAllDecorations().map((d) => d.id),
|
|
291
|
+
[],
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (placeholder) {
|
|
296
|
+
const placeholderDecorators = [
|
|
297
|
+
{
|
|
298
|
+
range: new monaco.Range(1, 1, 1, 1),
|
|
299
|
+
options: {
|
|
300
|
+
className: styles.placeholder,
|
|
301
|
+
isWholeLine: true,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
let decorators: string[] = [];
|
|
307
|
+
|
|
308
|
+
const checkDecorators = () => {
|
|
309
|
+
const model = editor.getModel();
|
|
310
|
+
if (!model) return;
|
|
311
|
+
|
|
312
|
+
const newDecorators = model.getValueLength() === 0 ? placeholderDecorators : [];
|
|
313
|
+
decorators = model.deltaDecorations(decorators, newDecorators);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
checkDecorators();
|
|
317
|
+
editor.onDidChangeModelContent(checkDecorators);
|
|
318
|
+
}
|
|
319
|
+
}, [enableAutocomplete, JSON.stringify(schemas), placeholder]);
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div
|
|
323
|
+
className={
|
|
324
|
+
'ant-input' +
|
|
325
|
+
(size ? ` ${SIZE_MAP[size].className}` : '') +
|
|
326
|
+
(disabled ? ` ant-input-disabled ${containerDisabledClassName}` : '') +
|
|
327
|
+
(readOnly ? ` ${containerReadOnlyClassName}` : '')
|
|
328
|
+
}
|
|
329
|
+
>
|
|
330
|
+
<div ref={containerRef}>
|
|
331
|
+
<MonacoEditor
|
|
332
|
+
width='100%'
|
|
333
|
+
height='100%'
|
|
334
|
+
language={YAML_LANG_ID}
|
|
335
|
+
theme={themeMap[theme]}
|
|
336
|
+
value={value}
|
|
337
|
+
onChange={onChange}
|
|
338
|
+
editorDidMount={handleEditorDidMount}
|
|
339
|
+
options={{
|
|
340
|
+
readOnly,
|
|
341
|
+
codeLens: false,
|
|
342
|
+
contextmenu: false,
|
|
343
|
+
fixedOverflowWidgets: true,
|
|
344
|
+
folding: true,
|
|
345
|
+
fontSize: 12,
|
|
346
|
+
lineDecorationsWidth: 0,
|
|
347
|
+
lineNumbers: 'on',
|
|
348
|
+
minimap: { enabled: false },
|
|
349
|
+
overviewRulerBorder: false,
|
|
350
|
+
overviewRulerLanes: 0,
|
|
351
|
+
padding: {
|
|
352
|
+
top: SIZE_MAP[size].top,
|
|
353
|
+
bottom: SIZE_MAP[size].bottom,
|
|
354
|
+
},
|
|
355
|
+
renderLineHighlight: 'line',
|
|
356
|
+
scrollbar: {
|
|
357
|
+
vertical: 'auto',
|
|
358
|
+
verticalScrollbarSize: 8,
|
|
359
|
+
horizontal: 'auto',
|
|
360
|
+
horizontalScrollbarSize: 8,
|
|
361
|
+
alwaysConsumeMouseWheel: false,
|
|
362
|
+
},
|
|
363
|
+
scrollBeyondLastLine: false,
|
|
364
|
+
suggest: {
|
|
365
|
+
showWords: true,
|
|
366
|
+
filterGraceful: true,
|
|
367
|
+
snippetsPreventQuickSuggestions: false,
|
|
368
|
+
shareSuggestSelections: false,
|
|
369
|
+
},
|
|
370
|
+
quickSuggestions: {
|
|
371
|
+
other: true,
|
|
372
|
+
comments: false,
|
|
373
|
+
strings: true,
|
|
374
|
+
},
|
|
375
|
+
quickSuggestionsDelay: 0,
|
|
376
|
+
suggestFontSize: 12,
|
|
377
|
+
wordWrap: 'on',
|
|
378
|
+
automaticLayout: true,
|
|
379
|
+
occurrencesHighlight: 'off',
|
|
380
|
+
}}
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface YamlCompletionItem {
|
|
2
|
+
label: string;
|
|
3
|
+
insertText: string;
|
|
4
|
+
detail?: string;
|
|
5
|
+
documentation?: string;
|
|
6
|
+
type: YamlCompletionType;
|
|
7
|
+
triggerOnInsert?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type YamlCompletionType = 'KEYWORD' | 'VALUE' | 'KEY' | 'BOOLEAN' | 'NUMBER' | 'STRING';
|
|
11
|
+
|
|
12
|
+
export interface YamlSchema {
|
|
13
|
+
properties?: Record<string, YamlSchemaProperty>;
|
|
14
|
+
required?: string[];
|
|
15
|
+
type?: string;
|
|
16
|
+
enum?: string[];
|
|
17
|
+
items?: YamlSchema;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface YamlSchemaProperty {
|
|
21
|
+
type?: string | string[];
|
|
22
|
+
description?: string;
|
|
23
|
+
enum?: string[];
|
|
24
|
+
enumDescriptions?: string[];
|
|
25
|
+
properties?: Record<string, YamlSchemaProperty>;
|
|
26
|
+
items?: YamlSchema;
|
|
27
|
+
required?: string[];
|
|
28
|
+
default?: any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface YamlEditorSchema {
|
|
32
|
+
schema: YamlSchema;
|
|
33
|
+
name?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface YamlValidationError {
|
|
38
|
+
message: string;
|
|
39
|
+
startLineNumber: number;
|
|
40
|
+
endLineNumber: number;
|
|
41
|
+
startColumn: number;
|
|
42
|
+
endColumn: number;
|
|
43
|
+
severity?: 'error' | 'warning' | 'info';
|
|
44
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { YamlValidationError, YamlEditorSchema } from './types';
|
|
2
|
+
|
|
3
|
+
export function validateYaml(content: string, schemas: YamlEditorSchema[] = []): YamlValidationError[] {
|
|
4
|
+
const errors: YamlValidationError[] = [];
|
|
5
|
+
|
|
6
|
+
if (!content.trim()) {
|
|
7
|
+
return errors;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
// 基础 YAML 语法验证
|
|
12
|
+
const lines = content.split('\n');
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < lines.length; i++) {
|
|
15
|
+
const line = lines[i];
|
|
16
|
+
const lineNumber = i + 1;
|
|
17
|
+
|
|
18
|
+
// 检查缩进一致性
|
|
19
|
+
if (line.trim() && !isValidIndentation(line)) {
|
|
20
|
+
errors.push({
|
|
21
|
+
message: 'Invalid indentation. YAML requires consistent indentation.',
|
|
22
|
+
startLineNumber: lineNumber,
|
|
23
|
+
endLineNumber: lineNumber,
|
|
24
|
+
startColumn: 1,
|
|
25
|
+
endColumn: line.length + 1,
|
|
26
|
+
severity: 'error',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 检查冒号后是否有空格
|
|
31
|
+
const colonMatch = line.match(/^(\s*)([^:\s]+)(:)(\S)/);
|
|
32
|
+
if (colonMatch) {
|
|
33
|
+
const startColumn = colonMatch[1].length + colonMatch[2].length + 2;
|
|
34
|
+
errors.push({
|
|
35
|
+
message: 'Missing space after colon. Use ": " instead of ":"',
|
|
36
|
+
startLineNumber: lineNumber,
|
|
37
|
+
endLineNumber: lineNumber,
|
|
38
|
+
startColumn: startColumn,
|
|
39
|
+
endColumn: startColumn + 1,
|
|
40
|
+
severity: 'error',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 检查列表项格式
|
|
45
|
+
const listMatch = line.match(/^(\s*)-(\S)/);
|
|
46
|
+
if (listMatch) {
|
|
47
|
+
const startColumn = listMatch[1].length + 2;
|
|
48
|
+
errors.push({
|
|
49
|
+
message: 'Missing space after dash. Use "- " instead of "-"',
|
|
50
|
+
startLineNumber: lineNumber,
|
|
51
|
+
endLineNumber: lineNumber,
|
|
52
|
+
startColumn: startColumn,
|
|
53
|
+
endColumn: startColumn + 1,
|
|
54
|
+
severity: 'error',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Schema 验证
|
|
60
|
+
if (schemas.length > 0) {
|
|
61
|
+
const schemaErrors = validateAgainstSchemas(content, schemas);
|
|
62
|
+
errors.push(...schemaErrors);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
errors.push({
|
|
66
|
+
message: `YAML parsing error: ${error}`,
|
|
67
|
+
startLineNumber: 1,
|
|
68
|
+
endLineNumber: 1,
|
|
69
|
+
startColumn: 1,
|
|
70
|
+
endColumn: 1,
|
|
71
|
+
severity: 'error',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return errors;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isValidIndentation(line: string): boolean {
|
|
79
|
+
const trimmed = line.trim();
|
|
80
|
+
if (!trimmed) return true;
|
|
81
|
+
|
|
82
|
+
const leadingSpaces = line.length - line.trimLeft().length;
|
|
83
|
+
// YAML 通常使用 2 或 4 个空格缩进
|
|
84
|
+
return leadingSpaces % 2 === 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateAgainstSchemas(_content: string, _schemas: YamlEditorSchema[]): YamlValidationError[] {
|
|
88
|
+
const errors: YamlValidationError[] = [];
|
|
89
|
+
|
|
90
|
+
// TODO: 实现更复杂的 schema 验证逻辑
|
|
91
|
+
// 目前提供基础的结构验证
|
|
92
|
+
// 可以在这里添加对 content 和 schemas 的具体验证
|
|
93
|
+
|
|
94
|
+
return errors;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function parseYamlValue(value: string): any {
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
|
|
100
|
+
// Boolean values
|
|
101
|
+
if (['true', 'True', 'TRUE', 'yes', 'Yes', 'YES', 'on', 'On', 'ON'].includes(trimmed)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (['false', 'False', 'FALSE', 'no', 'No', 'NO', 'off', 'Off', 'OFF'].includes(trimmed)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Null values
|
|
109
|
+
if (['null', 'Null', 'NULL', '~'].includes(trimmed)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Number values
|
|
114
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
115
|
+
return parseInt(trimmed, 10);
|
|
116
|
+
}
|
|
117
|
+
if (/^-?\d+\.\d+$/.test(trimmed)) {
|
|
118
|
+
return parseFloat(trimmed);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// String values (remove quotes if present)
|
|
122
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
123
|
+
return trimmed.slice(1, -1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return trimmed;
|
|
127
|
+
}
|
package/src/yaml/yaml.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export const languageConfiguration = {
|
|
2
|
+
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
|
|
3
|
+
comments: {
|
|
4
|
+
lineComment: '#',
|
|
5
|
+
},
|
|
6
|
+
brackets: [
|
|
7
|
+
['{', '}'],
|
|
8
|
+
['[', ']'],
|
|
9
|
+
],
|
|
10
|
+
autoClosingPairs: [
|
|
11
|
+
{ open: '{', close: '}' },
|
|
12
|
+
{ open: '[', close: ']' },
|
|
13
|
+
{ open: '"', close: '"' },
|
|
14
|
+
{ open: "'", close: "'" },
|
|
15
|
+
],
|
|
16
|
+
surroundingPairs: [
|
|
17
|
+
{ open: '{', close: '}' },
|
|
18
|
+
{ open: '[', close: ']' },
|
|
19
|
+
{ open: '"', close: '"' },
|
|
20
|
+
{ open: "'", close: "'" },
|
|
21
|
+
],
|
|
22
|
+
folding: {
|
|
23
|
+
offSide: true,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// YAML keywords
|
|
28
|
+
const keywords = ['true', 'false', 'null', 'True', 'False', 'Null', 'TRUE', 'FALSE', 'NULL', 'yes', 'no', 'Yes', 'No', 'YES', 'NO', 'on', 'off', 'On', 'Off', 'ON', 'OFF'];
|
|
29
|
+
|
|
30
|
+
// YAML operators and special characters (for future use)
|
|
31
|
+
// const operators = [':', '-', '|', '>', '&', '*', '!', '%', '@'];
|
|
32
|
+
|
|
33
|
+
export const language = {
|
|
34
|
+
tokenPostfix: '.yaml',
|
|
35
|
+
brackets: [
|
|
36
|
+
{ token: 'delimiter.bracket', open: '{', close: '}' },
|
|
37
|
+
{ token: 'delimiter.square', open: '[', close: ']' },
|
|
38
|
+
],
|
|
39
|
+
keywords: keywords,
|
|
40
|
+
numberInteger: /(?:0|[+-]?[0-9]+)/,
|
|
41
|
+
numberFloat: /(?:0|[+-]?[0-9]+)(?:\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/,
|
|
42
|
+
numberOctal: /0o[0-7]+/,
|
|
43
|
+
numberHex: /0x[0-9a-fA-F]+/,
|
|
44
|
+
numberInfinity: /[+-]?\.(?:inf|Inf|INF)/,
|
|
45
|
+
numberNaN: /\.(?:nan|Nan|NAN)/,
|
|
46
|
+
numberDate: /\d{4}-\d{2}-\d{2}(?:[Tt ]\d{1,2}:\d{2}:\d{2}(?:\.\d*)?(?:[ \t]*(?:Z|[-+]\d{1,2}(?::\d{2})?))?)?/,
|
|
47
|
+
escapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,
|
|
48
|
+
|
|
49
|
+
tokenizer: {
|
|
50
|
+
root: [
|
|
51
|
+
{ include: '@whitespace' },
|
|
52
|
+
{ include: '@comment' },
|
|
53
|
+
|
|
54
|
+
// Document markers
|
|
55
|
+
[/^---/, 'meta.document'],
|
|
56
|
+
[/^\.{3}/, 'meta.document'],
|
|
57
|
+
|
|
58
|
+
// Keys (property names)
|
|
59
|
+
[/^(\s*)([^:\s]+)(\s*)(:)/, ['white', 'key', 'white', 'delimiter']],
|
|
60
|
+
[/(\s*)([^:\s]+)(\s*)(:)/, ['white', 'key', 'white', 'delimiter']],
|
|
61
|
+
|
|
62
|
+
// Flow collections
|
|
63
|
+
[/\{/, '@brackets', '@flowMap'],
|
|
64
|
+
[/\[/, '@brackets', '@flowSequence'],
|
|
65
|
+
|
|
66
|
+
// Block scalars
|
|
67
|
+
[/[|>][-+]?(\d+)?/, 'string.block', '@blockScalar'],
|
|
68
|
+
|
|
69
|
+
// Numbers
|
|
70
|
+
[/@numberInfinity/, 'number.infinity'],
|
|
71
|
+
[/@numberNaN/, 'number.nan'],
|
|
72
|
+
[/@numberDate/, 'number.date'],
|
|
73
|
+
[/@numberHex/, 'number.hex'],
|
|
74
|
+
[/@numberOctal/, 'number.octal'],
|
|
75
|
+
[/@numberFloat/, 'number.float'],
|
|
76
|
+
[/@numberInteger/, 'number'],
|
|
77
|
+
|
|
78
|
+
// Strings
|
|
79
|
+
[/"([^"\\]|\\.)*$/, 'string.invalid'],
|
|
80
|
+
[/'([^'\\]|\\.)*$/, 'string.invalid'],
|
|
81
|
+
[/"/, 'string', '@doubleQuotedString'],
|
|
82
|
+
[/'/, 'string', '@singleQuotedString'],
|
|
83
|
+
|
|
84
|
+
// Anchors and aliases
|
|
85
|
+
[/&\w+/, 'tag.anchor'],
|
|
86
|
+
[/\*\w+/, 'tag.alias'],
|
|
87
|
+
|
|
88
|
+
// Tags
|
|
89
|
+
[/![\w\-\/]+/, 'tag'],
|
|
90
|
+
|
|
91
|
+
// Keywords
|
|
92
|
+
[
|
|
93
|
+
/[^\s\[\{\,]+/,
|
|
94
|
+
{
|
|
95
|
+
cases: {
|
|
96
|
+
'@keywords': 'keyword',
|
|
97
|
+
'@default': 'string.unquoted',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
|
|
102
|
+
// List items
|
|
103
|
+
[/^(\s*)(-\s)/, ['white', 'delimiter']],
|
|
104
|
+
[/(\s*)(-\s)/, ['white', 'delimiter']],
|
|
105
|
+
],
|
|
106
|
+
|
|
107
|
+
whitespace: [[/[ \t\r\n]+/, 'white']],
|
|
108
|
+
|
|
109
|
+
comment: [[/#.*$/, 'comment']],
|
|
110
|
+
|
|
111
|
+
doubleQuotedString: [
|
|
112
|
+
[/[^\\"]+/, 'string'],
|
|
113
|
+
[/@escapes/, 'string.escape'],
|
|
114
|
+
[/\\./, 'string.escape.invalid'],
|
|
115
|
+
[/"/, 'string', '@pop'],
|
|
116
|
+
],
|
|
117
|
+
|
|
118
|
+
singleQuotedString: [
|
|
119
|
+
[/[^\\']+/, 'string'],
|
|
120
|
+
[/''/, 'string.escape'],
|
|
121
|
+
[/'/, 'string', '@pop'],
|
|
122
|
+
],
|
|
123
|
+
|
|
124
|
+
blockScalar: [
|
|
125
|
+
[/^(\s*).+$/, 'string.block'],
|
|
126
|
+
[/^(?!\s)/, '', '@pop'],
|
|
127
|
+
],
|
|
128
|
+
|
|
129
|
+
flowMap: [
|
|
130
|
+
{ include: '@whitespace' },
|
|
131
|
+
{ include: '@comment' },
|
|
132
|
+
[/\}/, '@brackets', '@pop'],
|
|
133
|
+
[/,/, 'delimiter'],
|
|
134
|
+
[/:/, 'delimiter'],
|
|
135
|
+
[/"/, 'string', '@doubleQuotedString'],
|
|
136
|
+
[/'/, 'string', '@singleQuotedString'],
|
|
137
|
+
[/[^\s\,\}\:]+/, 'string.unquoted'],
|
|
138
|
+
],
|
|
139
|
+
|
|
140
|
+
flowSequence: [
|
|
141
|
+
{ include: '@whitespace' },
|
|
142
|
+
{ include: '@comment' },
|
|
143
|
+
[/\]/, '@brackets', '@pop'],
|
|
144
|
+
[/,/, 'delimiter'],
|
|
145
|
+
[/"/, 'string', '@doubleQuotedString'],
|
|
146
|
+
[/'/, 'string', '@singleQuotedString'],
|
|
147
|
+
[/[^\s\,\]]+/, 'string.unquoted'],
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
};
|