feat: add xml linter to code mirror (#306)

This commit is contained in:
Kristin Aoki
2023-04-14 11:58:49 -04:00
committed by GitHub
parent 4b7b1c91ec
commit 1d2a4c212d
6 changed files with 93 additions and 12 deletions

29
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",
"@codemirror/lint": "^6.2.1",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@reduxjs/toolkit": "^1.8.1",
@@ -31,7 +32,8 @@
"reselect": "^4.1.5",
"tinymce": "^5.10.4",
"video-react": "^0.15.0",
"video.js": "^7.18.1"
"video.js": "^7.18.1",
"xmlchecker": "^0.1.0"
},
"devDependencies": {
"@edx/frontend-build": "^11.0.2",
@@ -2824,9 +2826,9 @@
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
},
"node_modules/@codemirror/lint": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.1.0.tgz",
"integrity": "sha512-mdvDQrjRmYPvQ3WrzF6Ewaao+NWERYtpthJvoQ3tK3t/44Ynhk8ZGjTSL9jMEv8CgSMogmt75X8ceOZRDSXHtQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.2.1.tgz",
"integrity": "sha512-y1muai5U/uUPAGRyHMx9mHuHLypPcHWxzlZGknp/U5Mdb5Ol8Q5ZLp67UqyTbNFJJ3unVxZ8iX3g1fMN79S1JQ==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
@@ -34451,6 +34453,14 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/xmlchecker": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/xmlchecker/-/xmlchecker-0.1.0.tgz",
"integrity": "sha512-ElfXP7Fse9G37go0mjD5a7zY9fnE19BHk1qbH02uXX/O6Gi5Be2TPCg3zHG7ZIzV2yoGr/Gybfw/Z2Cs62tDxw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -36532,9 +36542,9 @@
}
},
"@codemirror/lint": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.1.0.tgz",
"integrity": "sha512-mdvDQrjRmYPvQ3WrzF6Ewaao+NWERYtpthJvoQ3tK3t/44Ynhk8ZGjTSL9jMEv8CgSMogmt75X8ceOZRDSXHtQ==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.2.1.tgz",
"integrity": "sha512-y1muai5U/uUPAGRyHMx9mHuHLypPcHWxzlZGknp/U5Mdb5Ol8Q5ZLp67UqyTbNFJJ3unVxZ8iX3g1fMN79S1JQ==",
"requires": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
@@ -60979,6 +60989,11 @@
}
}
},
"xmlchecker": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/xmlchecker/-/xmlchecker-0.1.0.tgz",
"integrity": "sha512-ElfXP7Fse9G37go0mjD5a7zY9fnE19BHk1qbH02uXX/O6Gi5Be2TPCg3zHG7ZIzV2yoGr/Gybfw/Z2Cs62tDxw=="
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -59,6 +59,7 @@
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",
"@codemirror/lint": "^6.2.1",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@reduxjs/toolkit": "^1.8.1",
@@ -79,7 +80,8 @@
"reselect": "^4.1.5",
"tinymce": "^5.10.4",
"video-react": "^0.15.0",
"video.js": "^7.18.1"
"video.js": "^7.18.1",
"xmlchecker": "^0.1.0"
},
"peerDependencies": {
"@edx/frontend-platform": ">1.15.0",

View File

@@ -115,7 +115,7 @@ exports[`EditorFooter render snapshot: save failed. Show error message 1`] = `
show={true}
>
<FormattedMessage
defaultMessage="Error: Content save failed. Try again later."
defaultMessage="Error: Content save failed. Please check recent changes and try again later."
description="Error message displayed when content fails to save."
id="authoring.editorfooter.save.error"
/>

View File

@@ -4,7 +4,7 @@ const messages = defineMessages({
contentSaveFailed: {
id: 'authoring.editorfooter.save.error',
defaultMessage: 'Error: Content save failed. Try again later.',
defaultMessage: 'Error: Content save failed. Please check recent changes and try again later.',
description: 'Error message displayed when content fails to save.',
},
cancelButtonAriaLabel: {

View File

@@ -1,11 +1,12 @@
import React, { useEffect } from 'react';
import xmlChecker from 'xmlchecker';
import { basicSetup } from 'codemirror';
import { EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { html } from '@codemirror/lang-html';
import { xml } from '@codemirror/lang-xml';
import { linter } from '@codemirror/lint';
import alphanumericMap from './constants';
import './index.scss';
@@ -27,6 +28,32 @@ export const cleanHTML = ({ initialText }) => {
return initialText.replace(translateRegex, translator);
};
export const syntaxChecker = ({ textArr, lang }) => {
const diagnostics = [];
if (lang === 'xml') {
const docString = textArr.join('\n');
const xmlDoc = `<?xml version="1.0" encoding="UTF-8"?> ${docString}`;
try {
xmlChecker.check(xmlDoc);
} catch (error) {
let errorStart = 0;
for (let i = 0; i < error.line - 1; i++) {
errorStart += textArr[i].length;
}
const errorLine = error.line;
const errorEnd = errorStart + textArr[errorLine - 1].length;
diagnostics.push({
from: errorStart,
to: errorEnd,
severity: 'error',
message: `${error.name}: ${error.message}`,
});
}
}
return diagnostics;
};
export const createCodeMirrorDomNode = ({
ref,
initialText,
@@ -38,7 +65,15 @@ export const createCodeMirrorDomNode = ({
const cleanText = cleanHTML({ initialText });
const newState = EditorState.create({
doc: cleanText,
extensions: [basicSetup, languageExtension, EditorView.lineWrapping],
extensions: [
basicSetup,
languageExtension,
EditorView.lineWrapping,
linter((view) => {
const textArr = view.docView.view.viewState.state.doc.text;
return syntaxChecker({ textArr, lang });
}),
],
});
const view = new EditorView({ state: newState, parent: ref.current });
// eslint-disable-next-line no-param-reassign

View File

@@ -114,6 +114,35 @@ describe('CodeEditor', () => {
});
});
});
describe('xmlSyntaxChecker', () => {
describe('lang equals html', () => {
it('returns empty array', () => {
const textArr = ['<problem>', '<p>', 'this is some text', '</p>', '</problem>'];
const diagnostics = hooks.syntaxChecker({ textArr, lang: 'html' });
expect(diagnostics).toEqual([]);
});
});
describe('lang equals xml', () => {
it('returns empty array', () => {
const textArr = ['<problem>', '<p>', 'this is some text', '</p>', '</problem>'];
const diagnostics = hooks.syntaxChecker({ textArr, lang: 'xml' });
expect(diagnostics).toEqual([]);
});
it('returns an array with error object', () => {
const textArr = ['<problem>', '<p>', '<p>', 'this is some text', '</p>', '</problem>'];
const expectedDiagnostics = hooks.syntaxChecker({ textArr, lang: 'xml' });
const diagnostics = [{
from: 9,
to: 12,
severity: 'error',
message: 'SyntaxError: Expected that start and end tag must be identical but "<" found.',
}];
expect(expectedDiagnostics).toEqual(diagnostics);
});
});
});
describe('Component', () => {
describe('Snapshots', () => {
const mockHideBtn = jest.fn().mockName('mockHidebtn');