diff --git a/package-lock.json b/package-lock.json index 353f2b28..9ce3945d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,7 @@ "deserialize-error": "0.0.3", "dompurify": "^2.5.6", "fast-json-patch": "^3.1.1", + "fast-xml-parser": "^5.2.3", "graphql": "^15.8.0", "har-validator": "^5.1.3", "http-encoding": "^2.0.1", @@ -8990,6 +8991,24 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fast-xml-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.3.tgz", + "integrity": "sha512-OdCYfRqfpuLUFonTNjvd30rCBZUneHpSQkCqfaeWQ9qrKcl6XlWeDBNVwGb+INAIxRshuN2jF+BE0L6gbBO2mw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -18714,6 +18733,18 @@ "node": ">=0.10.0" } }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/style-loader": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", @@ -28606,6 +28637,14 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "fast-xml-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.3.tgz", + "integrity": "sha512-OdCYfRqfpuLUFonTNjvd30rCBZUneHpSQkCqfaeWQ9qrKcl6XlWeDBNVwGb+INAIxRshuN2jF+BE0L6gbBO2mw==", + "requires": { + "strnum": "^2.1.0" + } + }, "fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -35934,6 +35973,11 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" }, + "strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==" + }, "style-loader": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", diff --git a/package.json b/package.json index 4bc9698a..92ee88da 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "deserialize-error": "0.0.3", "dompurify": "^2.5.6", "fast-json-patch": "^3.1.1", + "fast-xml-parser": "^5.2.3", "graphql": "^15.8.0", "har-validator": "^5.1.3", "http-encoding": "^2.0.1", diff --git a/src/components/editor/monaco.ts b/src/components/editor/monaco.ts index b79f4183..245345dd 100644 --- a/src/components/editor/monaco.ts +++ b/src/components/editor/monaco.ts @@ -6,6 +6,7 @@ import { defineMonacoThemes } from '../../styles'; import { delay } from '../../util/promise'; import { asError } from '../../util/error'; import { observable, runInAction } from 'mobx'; +import { setupXMLValidation } from './xml-validation'; export type { MonacoTypes, @@ -74,6 +75,8 @@ async function loadMonacoEditor(retries = 5): Promise { }, }); + setupXMLValidation(monaco); + MonacoEditor = rmeModule.default; } catch (err) { console.log('Monaco load failed', asError(err).message); diff --git a/src/components/editor/xml-validation.ts b/src/components/editor/xml-validation.ts new file mode 100644 index 00000000..f3f4787c --- /dev/null +++ b/src/components/editor/xml-validation.ts @@ -0,0 +1,52 @@ +import type * as MonacoTypes from 'monaco-editor' +import { XMLValidator } from 'fast-xml-parser' + +export function setupXMLValidation(monaco: typeof MonacoTypes) { + const markerId = 'xml-validation' + const contentChangeListeners = new Map() + + monaco.editor.onWillDisposeModel(model => { + contentChangeListeners.delete(model) + }) + + function validate(model: MonacoTypes.editor.ITextModel) { + const markers: MonacoTypes.editor.IMarkerData[] = [] + const text = model.getValue() + + if (text.trim()) { + const validationResult = XMLValidator.validate(text, { + allowBooleanAttributes: true, + }) + + if (validationResult !== true) { + markers.push({ + severity: monaco.MarkerSeverity.Error, + startLineNumber: validationResult.err.line, + startColumn: validationResult.err.col, + endLineNumber: validationResult.err.line, + endColumn: model.getLineContent(validationResult.err.line).length + 1, + message: validationResult.err.msg, + }) + } + } + + monaco.editor.setModelMarkers(model, markerId, markers) + } + + monaco.editor.onDidChangeModelLanguage(({ model }) => { + const isXml = model.getModeId() === 'xml' + const listener = contentChangeListeners.get(model) + + if (isXml && !listener) { + contentChangeListeners.set( + model, + model.onDidChangeContent(() => validate(model)) + ) + validate(model) + } else if (!isXml && listener) { + listener.dispose() + contentChangeListeners.delete(model) + monaco.editor.setModelMarkers(model, markerId, []) + } + }) +}