From 26ea1b0d1b882294df380dca8cd6f740c576df75 Mon Sep 17 00:00:00 2001 From: ceifa Date: Wed, 4 Jun 2025 22:31:46 -0300 Subject: [PATCH 1/2] feat: add xml content type custom validation --- package-lock.json | 44 +++++++++++++++++++++++++ package.json | 1 + src/components/editor/monaco.ts | 3 ++ src/components/editor/xml-validation.ts | 40 ++++++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 src/components/editor/xml-validation.ts diff --git a/package-lock.json b/package-lock.json index a12855ee..2358cc8f 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", @@ -9008,6 +9009,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", @@ -18690,6 +18709,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", @@ -28561,6 +28592,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", @@ -35890,6 +35929,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 8695cd7e..a56ca2f0 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..245aaaf2 --- /dev/null +++ b/src/components/editor/xml-validation.ts @@ -0,0 +1,40 @@ +import type * as MonacoTypes from 'monaco-editor' +import { XMLValidator } from 'fast-xml-parser' + +export function setupXMLValidation(monaco: typeof MonacoTypes) { + const xmlModels = new Set() + + monaco.editor.onWillDisposeModel(model => { + xmlModels.delete(model) + }) + + monaco.editor.onDidChangeModelLanguage(event => { + const model = event.model + if (model.getModeId() === 'xml' && !xmlModels.has(model)) { + xmlModels.add(model) + model.onDidChangeContent(() => { + 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: validationResult.err.col + 10, // the 10 is totally arbitrary here + message: validationResult.err.msg, + }) + } + } + + monaco.editor.setModelMarkers(model, 'xml-validation', markers) + }) + } + }) +} From 351c92902bec016445579477356c8652cc3cbd80 Mon Sep 17 00:00:00 2001 From: ceifa Date: Thu, 5 Jun 2025 09:35:53 -0300 Subject: [PATCH 2/2] fix: xml validation not turning off on other content types --- src/components/editor/xml-validation.ts | 68 +++++++++++++++---------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/src/components/editor/xml-validation.ts b/src/components/editor/xml-validation.ts index 245aaaf2..f3f4787c 100644 --- a/src/components/editor/xml-validation.ts +++ b/src/components/editor/xml-validation.ts @@ -2,39 +2,51 @@ import type * as MonacoTypes from 'monaco-editor' import { XMLValidator } from 'fast-xml-parser' export function setupXMLValidation(monaco: typeof MonacoTypes) { - const xmlModels = new Set() + const markerId = 'xml-validation' + const contentChangeListeners = new Map() monaco.editor.onWillDisposeModel(model => { - xmlModels.delete(model) + contentChangeListeners.delete(model) }) - monaco.editor.onDidChangeModelLanguage(event => { - const model = event.model - if (model.getModeId() === 'xml' && !xmlModels.has(model)) { - xmlModels.add(model) - model.onDidChangeContent(() => { - 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: validationResult.err.col + 10, // the 10 is totally arbitrary here - message: validationResult.err.msg, - }) - } - } - - monaco.editor.setModelMarkers(model, 'xml-validation', markers) + 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, []) } }) }