main
vue 253 lines 5.64 KB
Raw
1 <template>
2 <codemirror
3 v-model="code"
4 placeholder="Code goes here..."
5 autofocus
6 indent-with-tab
7 :tab-size="4"
8 :extensions
9 :style="{ height: '100%' }"
10 @ready="handleReady"
11 />
12 </template>
13
14 <script setup lang="ts">
15 // EVALUATE: https://www.npmjs.com/package/vue3-ace-editor
16 // EVALUATE: https://github.surmon.me/vue-codemirror
17 // EVALUATE: https://www.npmjs.com/package/@guolao/vue-monaco-editor
18
19 import type { Diagnostic } from "@codemirror/lint"
20 import type { Extension } from "@codemirror/state"
21 import { redo, redoDepth, undo, undoDepth } from "@codemirror/commands"
22 import { xml } from "@codemirror/lang-xml"
23 import { linter } from "@codemirror/lint"
24 import { oneDark } from "@codemirror/theme-one-dark"
25 import { EditorView } from "@codemirror/view"
26 import { XMLValidator } from "fast-xml-parser"
27 import _isEqual from "lodash/isEqual"
28 import _trim from "lodash/trim"
29 import _uniqWith from "lodash/uniqWith"
30 import { tomorrow } from "thememirror"
31 import { computed, onMounted, ref, shallowRef, watch } from "vue"
32 import { Codemirror } from "vue-codemirror"
33 import * as xmllint from "xmllint-wasm"
34 import { useThemeStore } from "@/stores/theme"
35
36 export interface XMLEditorCtx {
37 undo: () => void
38 redo: () => void
39 scrollToLine: (line: number) => void
40 canUndo: () => boolean
41 canRedo: () => boolean
42 }
43
44 export interface XMLError {
45 line: number
46 column: number
47 message: string
48 level: "error"
49 }
50
51 const emit = defineEmits<{
52 (e: "mounted", value: XMLEditorCtx): void
53 (e: "errors", value: XMLError[]): void
54 }>()
55
56 const code = defineModel<string>("code", { default: "" })
57
58 const themeStore = useThemeStore()
59 const isDark = computed<boolean>(() => themeStore.isThemeDark)
60
61 function convertXMLErrorsToDiagnostics(errors: XMLError[], text: string): Diagnostic[] {
62 const diagnostics: Diagnostic[] = []
63 const lines = text.split("\n")
64
65 emit(
66 "errors",
67 errors
68 .map(o => ({ ...o, message: _trim(o.message) }))
69 .filter(o => o.message !== "^")
70 .sort((a, b) => a.line - b.line)
71 )
72
73 errors.forEach(error => {
74 // Calculate position in text
75 let from = 0
76 for (let i = 0; i < error.line - 1; i++) {
77 from += (lines[i]?.length ?? 0) + 1 // +1 for newline
78 }
79 from += error.column - 1
80
81 // Find the end of the error (end of line or end of message)
82 const lineText = lines[error.line - 1] || ""
83 const to = from + Math.min(lineText.length - (error.column - 1), 50) // Limit to 50 characters
84
85 diagnostics.push({
86 from,
87 to,
88 severity: error.level,
89 message: error.message
90 })
91 })
92
93 return diagnostics
94 }
95
96 async function strategyXMLLint(text: string): Promise<XMLError[]> {
97 try {
98 const result = await xmllint.validateXML({
99 xml: text,
100 // Optional: Initial memory capacity in Web Assembly memory pages (1 = 6.4KiB) - 256
101 // is minimum and default here (16MiB).
102 initialMemoryPages: 256,
103 // Optional: Maximum memory capacity, in Web Assembly memory pages. If not
104 // set, this will also default to 256 pages. Max is 65536 (4GiB).
105 // Use this to raise the memory limit if your XML to validate are large enough to
106 // cause out of memory errors.
107 // The following example would set the max memory to 2GiB.
108 maxMemoryPages: 2 * xmllint.memoryPages.GiB,
109 normalization: "format"
110 })
111
112 if (result.valid) {
113 return []
114 }
115
116 const errors: XMLError[] = result.errors.map(error => ({
117 line: error.loc?.lineNumber || 1,
118 column: 1,
119 message: error.message,
120 level: "error"
121 }))
122
123 return errors
124 } catch (err) {
125 console.error(err)
126 return []
127 }
128 }
129
130 async function strategyFastXMLParser(text: string): Promise<XMLError[]> {
131 try {
132 const errors: XMLError[] = []
133
134 const validation = XMLValidator.validate(text)
135
136 if (validation === true) {
137 return []
138 }
139
140 if (typeof validation === "object" && validation.err) {
141 const error = validation.err
142
143 errors.push({
144 line: error.line || 1,
145 column: error.col || 1,
146 message: error.msg,
147 level: "error"
148 })
149 }
150
151 return errors
152 } catch {
153 return []
154 }
155 }
156
157 async function validateXML(text: string): Promise<Diagnostic[]> {
158 let errors: XMLError[] = []
159
160 try {
161 errors = await strategyXMLLint(text)
162 } catch (err) {
163 console.error(err)
164
165 try {
166 errors = await strategyFastXMLParser(text)
167 } catch (err) {
168 console.error(err)
169 }
170 }
171
172 return convertXMLErrorsToDiagnostics(_uniqWith(errors, _isEqual), text)
173 }
174
175 const extensions = computed(() => {
176 const list: Extension[] = [xml()]
177
178 if (isDark.value) {
179 list.push(oneDark)
180 } else {
181 list.push(tomorrow)
182 }
183
184 list.push(
185 linter(async view => {
186 const text = view.state.doc.toString()
187
188 if (!text.trim()) {
189 return []
190 }
191
192 return await validateXML(text)
193 })
194 )
195
196 return list
197 })
198
199 const cmView = shallowRef<EditorView | null>(null)
200 const canUndo = ref<boolean>(false)
201 const canRedo = ref<boolean>(false)
202
203 function updateHistoryState() {
204 canUndo.value = cmView.value ? !!undoDepth(cmView.value.state) : false
205 canRedo.value = cmView.value ? !!redoDepth(cmView.value.state) : false
206 }
207
208 function handleReady({ view }: { view: EditorView }) {
209 cmView.value = view
210 }
211
212 function handleUndo() {
213 if (cmView.value) {
214 undo({
215 state: cmView.value.state,
216 dispatch: cmView.value.dispatch
217 })
218 }
219 }
220
221 function handleRedo() {
222 if (cmView.value) {
223 redo({
224 state: cmView.value.state,
225 dispatch: cmView.value.dispatch
226 })
227 }
228 }
229
230 function scrollToLine(line: number) {
231 if (cmView.value) {
232 const view = cmView.value
233 const lineInfo = view.state.doc.line(line)
234 view.dispatch({
235 effects: EditorView.scrollIntoView(lineInfo.from, { y: "center" })
236 })
237 }
238 }
239
240 watch(code, () => {
241 updateHistoryState()
242 })
243
244 onMounted(() => {
245 emit("mounted", {
246 undo: handleUndo,
247 redo: handleRedo,
248 scrollToLine,
249 canRedo: () => canRedo.value,
250 canUndo: () => canUndo.value
251 })
252 })
253 </script>