11<script setup lang="ts">
22import { useI18n } from ' @tap/i18n'
3+ import JSON5 from ' json5'
34import * as monaco from ' monaco-editor'
4- import { onBeforeUnmount , ref } from ' vue'
5+ import { ref } from ' vue'
56import MonacoEditor from ' ./MonacoEditor.vue'
67
78const { t } = useI18n ()
9+ const JSON5_LANGUAGE_ID = ' json5'
810
911const props = defineProps ({
1012 height: {
@@ -35,7 +37,73 @@ const editorValue = defineModel('modelValue', {
3537 type: String ,
3638 default: ' ' ,
3739})
38- const monacoEditorRef = ref (null )
40+ const monacoEditorRef = ref <any >(null )
41+
42+ let json5LanguageRegistered = false
43+
44+ const registerJson5Language = () => {
45+ if (json5LanguageRegistered ) return
46+
47+ monaco .languages .register ({
48+ id: JSON5_LANGUAGE_ID ,
49+ extensions: [' .json5' ],
50+ aliases: [' JSON5' , ' json5' ],
51+ })
52+
53+ monaco .languages .setLanguageConfiguration (JSON5_LANGUAGE_ID , {
54+ comments: {
55+ lineComment: ' //' ,
56+ blockComment: [' /*' , ' */' ],
57+ },
58+ brackets: [
59+ [' {' , ' }' ],
60+ [' [' , ' ]' ],
61+ ],
62+ autoClosingPairs: [
63+ { open: ' {' , close: ' }' },
64+ { open: ' [' , close: ' ]' },
65+ { open: ' (' , close: ' )' },
66+ { open: ' "' , close: ' "' , notIn: [' string' , ' comment' ] },
67+ { open: " '" , close: " '" , notIn: [' string' , ' comment' ] },
68+ ],
69+ surroundingPairs: [
70+ { open: ' {' , close: ' }' },
71+ { open: ' [' , close: ' ]' },
72+ { open: ' (' , close: ' )' },
73+ { open: ' "' , close: ' "' },
74+ { open: " '" , close: " '" },
75+ ],
76+ })
77+
78+ monaco .languages .setMonarchTokensProvider (JSON5_LANGUAGE_ID , {
79+ defaultToken: ' ' ,
80+ tokenPostfix: ' .json5' ,
81+ tokenizer: {
82+ root: [
83+ [/ [ \t\r\n ] + / , ' white' ],
84+ [/ \/\/ . * $ / , ' comment' ],
85+ [/ \/\* / , { token: ' comment' , next: ' @comment' }],
86+ [/ \b (?:true| false| null| Infinity| NaN)\b / , ' keyword' ],
87+ [/ [+\- ] ? (?:0x[0-9a-f ] + | (?:\d + \. \d * | \. \d + | \d + )(?:e[+\- ] ? \d + )? )/ i , ' number' ],
88+ [/ '(?:[^ '\\ ] | \\ . )* '/ , ' string' ],
89+ [/ "(?:[^ "\\ ] | \\ . )* "/ , ' string' ],
90+ [/ [A-Z _$][\w $] * / i , ' identifier' ],
91+ [/ [{}[\] ] / , ' @brackets' ],
92+ [/ [:,] / , ' delimiter' ],
93+ [/ . / , ' delimiter.invalid' ],
94+ ],
95+ comment: [
96+ [/ [^ /*] + / , ' comment' ],
97+ [/ \*\/ / , { token: ' comment' , next: ' @pop' }],
98+ [/ [/*] / , ' comment' ],
99+ ],
100+ },
101+ })
102+
103+ json5LanguageRegistered = true
104+ }
105+
106+ registerJson5Language ()
39107
40108const mongoOperators = [
41109 // Comparison operators
@@ -141,16 +209,17 @@ const mongoOperators = [
141209 },
142210]
143211
144- const registerMongoCompletion = () => {
145- return monaco .languages .registerCompletionItemProvider (' json ' , {
212+ const registerMongoCompletion = (languageId : string ) => {
213+ return monaco .languages .registerCompletionItemProvider (languageId , {
146214 triggerCharacters: [' $' , ' "' , " '" , ' {' ],
147- provideCompletionItems : (model , position ) => {
215+ provideCompletionItems : (model : any , position : any ) => {
148216 const word = model .getWordUntilPosition (position )
149217 const lineContent = model .getLineContent (position .lineNumber )
150218 const textBeforeCursor = lineContent .slice (
151219 0 ,
152220 Math .max (0 , position .column - 1 ),
153221 )
222+ const isJson5 = model .getLanguageId () === JSON5_LANGUAGE_ID
154223
155224 const range = {
156225 startLineNumber: position .lineNumber ,
@@ -159,7 +228,7 @@ const registerMongoCompletion = () => {
159228 endColumn: word .endColumn ,
160229 }
161230
162- const suggestions = []
231+ const suggestions: any [] = []
163232
164233 if (lineContent ?.trim () === ' {}' ) return { suggestions }
165234
@@ -200,14 +269,14 @@ const registerMongoCompletion = () => {
200269 label: op .label ,
201270 kind: op .kind ,
202271 detail: op .detail ,
203- insertText: isInQuotes ? op .label : ` "${op .label }" ` ,
272+ insertText: isInQuotes || isJson5 ? op .label : ` "${op .label }" ` ,
204273 range: replaceRange ,
205274 sortText: ` 2${op .label } ` ,
206275 })),
207276 )
208277
209278 if (props .fields && props .fields .length > 0 ) {
210- const matchingFields = props .fields .filter ((field ) =>
279+ const matchingFields = ( props .fields as any []) .filter ((field : any ) =>
211280 field .field_name .toLowerCase ().startsWith (word .word .toLowerCase ()),
212281 )
213282 if (matchingFields .length > 0 ) {
@@ -216,9 +285,10 @@ const registerMongoCompletion = () => {
216285 label: field .field_name ,
217286 kind: monaco .languages .CompletionItemKind .Field ,
218287 detail: field .data_type ,
219- insertText: isInQuotes
220- ? field .field_name
221- : ` "${field .field_name }" ` ,
288+ insertText:
289+ isInQuotes || isJson5
290+ ? field .field_name
291+ : ` "${field .field_name }" ` ,
222292 range ,
223293 sortText: ` 1${field .field_name } ` ,
224294 })),
@@ -227,7 +297,7 @@ const registerMongoCompletion = () => {
227297 }
228298
229299 if (props .variables && props .variables .length > 0 && word .word ) {
230- const matchingVariables = props .variables .filter ((variable ) =>
300+ const matchingVariables = ( props .variables as any []) .filter ((variable : any ) =>
231301 variable .name .toLowerCase ().startsWith (word .word .toLowerCase ()),
232302 )
233303
@@ -271,9 +341,10 @@ const registerMongoCompletion = () => {
271341 }
272342 } else {
273343 // 不在 {{}} 内部,需要完整的 {{variable}}
274- insertText = isInQuotes
275- ? ` {{${variable .name }}} `
276- : ` "{{${variable .name }}}" `
344+ insertText =
345+ isInQuotes || isJson5
346+ ? ` {{${variable .name }}} `
347+ : ` "{{${variable .name }}}" `
277348 }
278349
279350 return {
@@ -299,19 +370,21 @@ const registerMongoCompletion = () => {
299370}
300371
301372// 注册自动补全
302- let completionDisposable = null
373+ let completionRegistered = false
303374
304- if (typeof monaco !== ' undefined' ) {
305- completionDisposable = registerMongoCompletion ()
375+ if (typeof monaco !== ' undefined' && ! completionRegistered ) {
376+ completionRegistered = true
377+ registerMongoCompletion (' json' )
378+ registerMongoCompletion (JSON5_LANGUAGE_ID )
306379}
307380
308- const validateJSON = (jsonString ) => {
381+ const validateJSON = (jsonString : string ) => {
309382 if (! jsonString .trim ()) {
310383 return { isValid: true , error: null }
311384 }
312385
313386 try {
314- const parsed = JSON .parse (jsonString )
387+ const parsed = JSON5 .parse (jsonString )
315388 if (typeof parsed !== ' object' || parsed === null ) {
316389 return {
317390 isValid: false ,
@@ -323,32 +396,37 @@ const validateJSON = (jsonString) => {
323396 }
324397 }
325398 return { isValid: true , error: null }
326- } catch (syntaxError ) {
399+ } catch (syntaxError : any ) {
400+ const errorMessage = String (syntaxError ?.message ?? ' ' )
327401 return {
328402 isValid: false ,
329403 error: {
330- message: syntaxError . message ,
331- line: getErrorLine ( syntaxError .message ) ,
332- column: getErrorColumn ( syntaxError .message ) ,
404+ message: errorMessage ,
405+ line: syntaxError .lineNumber || getErrorLine ( errorMessage ) || 1 ,
406+ column: syntaxError .columnNumber || getErrorColumn ( errorMessage ) || 1 ,
333407 },
334408 }
335409 }
336410}
337411
338412// Extract line number from JSON parse error message
339- const getErrorLine = (errorMessage ) => {
413+ function getErrorLine(errorMessage : string ) {
340414 const lineMatch = errorMessage .match (/ line (\d + )/ i )
341- return lineMatch ? Number .parseInt (lineMatch [1 ]) : 1
415+ return Number .parseInt (lineMatch ?. [1 ] ?? ' 1 ' )
342416}
343417
344- const getErrorColumn = (errorMessage ) => {
418+ function getErrorColumn(errorMessage : string ) {
345419 const columnMatch = errorMessage .match (/ column (\d + )/ i )
346- return columnMatch ? Number .parseInt (columnMatch [1 ]) : 1
420+ return Number .parseInt (columnMatch ?. [1 ] ?? ' 1 ' )
347421}
348422
349- const validationError = ref (null )
423+ const validationError = ref <{
424+ message: string
425+ line: number
426+ column: number
427+ } | null >(null )
350428
351- const handleChange = (val ) => {
429+ const handleChange = (val : string ) => {
352430 const validation = validateJSON (val )
353431 validationError .value = validation .error
354432
@@ -360,20 +438,32 @@ const handleChange = (val) => {
360438 })
361439}
362440
363- const formatCode = () => {
364- if (monacoEditorRef .value ) {
365- monacoEditorRef .value .format ()
441+ const normalizeJSON = (jsonString : string ) => {
442+ if (! jsonString .trim ()) {
443+ return ' '
444+ }
445+
446+ try {
447+ const parsed = JSON5 .parse (jsonString )
448+ if (typeof parsed !== ' object' || parsed === null ) {
449+ return null
450+ }
451+ return JSON .stringify (parsed , null , 2 )
452+ } catch {
453+ return null
366454 }
367455}
368456
369- onBeforeUnmount (() => {
370- if (completionDisposable ) {
371- completionDisposable .dispose ()
457+ const formatCode = () => {
458+ const normalized = normalizeJSON (editorValue .value )
459+ if (normalized !== null ) {
460+ editorValue .value = normalized
372461 }
373- })
462+ }
374463
375464defineExpose ({
376465 format: formatCode ,
466+ normalize: normalizeJSON ,
377467 getEditor : () => monacoEditorRef .value ?.getEditor (),
378468 validateJSON ,
379469})
@@ -389,7 +479,7 @@ defineExpose({
389479 ref="monacoEditorRef"
390480 v-model =" editorValue "
391481 :options =" {
392- language: ' json ' ,
482+ language: ' json5 ' ,
393483 automaticLayout: true ,
394484 minimap: { enabled: false },
395485 scrollBeyondLastLine: false ,
@@ -407,6 +497,7 @@ defineExpose({
407497 formatOnType: false ,
408498 ... options ,
409499 } "
500+ :height =" height "
410501 @change =" handleChange "
411502 />
412503 </div >
0 commit comments