diff --git a/src/editors/containers/TextEditor/components/CodeEditor/__snapshots__/index.test.jsx.snap b/src/editors/containers/TextEditor/components/CodeEditor/__snapshots__/index.test.jsx.snap
index dc2e3ceec..877112366 100644
--- a/src/editors/containers/TextEditor/components/CodeEditor/__snapshots__/index.test.jsx.snap
+++ b/src/editors/containers/TextEditor/components/CodeEditor/__snapshots__/index.test.jsx.snap
@@ -3,7 +3,18 @@
exports[`CodeEditor Component Snapshots Renders and calls Hooks 1`] = `
`;
diff --git a/src/editors/containers/TextEditor/components/CodeEditor/constants.js b/src/editors/containers/TextEditor/components/CodeEditor/constants.js
new file mode 100644
index 000000000..bb390430c
--- /dev/null
+++ b/src/editors/containers/TextEditor/components/CodeEditor/constants.js
@@ -0,0 +1,36 @@
+// HTML symbols to unescape in a Alphanumeric value : Literal mapping.
+const alphanumericMap = {
+ cent: '¢',
+ pound: '£',
+ sect: '§',
+ copy: '©',
+ laquo: '«',
+ raquo: '»',
+ reg: '®',
+ deg: '°',
+ plusmn: '±',
+ para: '¶',
+ middot: '·',
+ frac12: '½',
+ mdash: '—',
+ ndash: '–',
+ lsquo: '‘',
+ rsquo: '’',
+ sbquo: '‚',
+ rdquo: '”',
+ ldquo: '“',
+ dagger: '†',
+ Dagger: '‡',
+ bull: '•',
+ hellip: '…',
+ prime: '′',
+ Prime: '″',
+ euro: '€',
+ trade: '™',
+ asymp: '≈',
+ ne: '≠',
+ le: '≤',
+ ge: '≥',
+ quot: '"',
+};
+export default alphanumericMap;
diff --git a/src/editors/containers/TextEditor/components/CodeEditor/index.jsx b/src/editors/containers/TextEditor/components/CodeEditor/index.jsx
index b72b4324c..f4a465bd2 100644
--- a/src/editors/containers/TextEditor/components/CodeEditor/index.jsx
+++ b/src/editors/containers/TextEditor/components/CodeEditor/index.jsx
@@ -1,19 +1,38 @@
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
+import {
+ Button,
+} from '@edx/paragon';
+
import { basicSetup } from 'codemirror';
import { EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { html } from '@codemirror/lang-html';
+
+import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import alphanumericMap from './constants';
import * as module from './index';
+import messages from './messages';
import './index.scss';
export const hooks = {
+ state: {
+ showBtnEscapeHTML: (val) => React.useState(val),
+ },
+
+ prepareShowBtnEscapeHTML: () => {
+ const [visibility, setVisibility] = hooks.state.showBtnEscapeHTML(true);
+ const hide = () => setVisibility(false);
+ return { showBtnEscapeHTML: visibility, hideBtn: hide };
+ },
+
createCodeMirrorDomNode: ({ ref, initialText, upstreamRef }) => {
useEffect(() => {
+ const cleanText = hooks.cleanHTML({ initialText });
const state = EditorState.create({
- doc: initialText,
+ doc: cleanText,
extensions: [basicSetup, html(), EditorView.lineWrapping],
});
const view = new EditorView({ state, parent: ref.current });
@@ -26,17 +45,54 @@ export const hooks = {
};
}, []);
},
+ cleanHTML: ({ initialText }) => {
+ const translateRegex = new RegExp(`&(${Object.keys(alphanumericMap).join('|')});`, 'g');
+ const translator = ($0, $1) => alphanumericMap[$1];
+ return initialText.replace(translateRegex, translator);
+ },
+ escapeHTMLSpecialChars: ({ ref, hideBtn }) => {
+ const text = ref.current.state.doc.toString(); let
+ pos = 0;
+ const changes = [];
+ Object.keys(alphanumericMap).forEach(
+ (escapedKeyword) => {
+ // eslint-disable-next-line no-cond-assign
+ for (let next; (next = text.indexOf(alphanumericMap[escapedKeyword], pos)) > -1;) {
+ changes.push({ from: next, to: next + 1, insert: `&${escapedKeyword};` });
+ pos = next + 1;
+ }
+ },
+ );
+ ref.current.dispatch({ changes });
+ hideBtn();
+ },
+
};
export const CodeEditor = ({
- innerRef, value,
+ innerRef,
+ value,
+ // injected
+ intl,
}) => {
const DOMref = useRef();
+ const btnRef = useRef();
module.hooks.createCodeMirrorDomNode({ ref: DOMref, initialText: value, upstreamRef: innerRef });
+ const { showBtnEscapeHTML, hideBtn } = module.hooks.prepareShowBtnEscapeHTML();
return (
-
+
+ {showBtnEscapeHTML && (
+
+ )}
);
};
@@ -47,6 +103,7 @@ CodeEditor.propTypes = {
PropTypes.shape({ current: PropTypes.any }),
]).isRequired,
value: PropTypes.string.isRequired,
+ intl: intlShape.isRequired,
};
-export default CodeEditor;
+export default injectIntl(CodeEditor);
diff --git a/src/editors/containers/TextEditor/components/CodeEditor/index.test.jsx b/src/editors/containers/TextEditor/components/CodeEditor/index.test.jsx
index caa475a63..918818b99 100644
--- a/src/editors/containers/TextEditor/components/CodeEditor/index.test.jsx
+++ b/src/editors/containers/TextEditor/components/CodeEditor/index.test.jsx
@@ -4,6 +4,8 @@ import { shallow } from 'enzyme';
import { EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { html } from '@codemirror/lang-html';
+import { formatMessage, MockUseState } from '../../../../../testUtils';
+import alphanumericMap from './constants';
import * as module from './index';
jest.mock('@codemirror/view');
@@ -30,11 +32,59 @@ jest.mock('codemirror', () => ({
basicSetup: 'bAsiCSetUp',
}));
+const state = new MockUseState(module.hooks);
+
describe('CodeEditor', () => {
describe('Hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
+ state.testGetter(state.keys.showBtnEscapeHTML);
+ describe('stateHooks', () => {
+ beforeEach(() => {
+ state.mock();
+ });
+ it('prepareShowBtnEscapeHTML', () => {
+ const hook = module.hooks.prepareShowBtnEscapeHTML();
+ expect(state.stateVals.showBtnEscapeHTML).toEqual(hook.showBtnEscapeHTML);
+ hook.hideBtn();
+ expect(state.setState.showBtnEscapeHTML).toHaveBeenCalledWith(false);
+ });
+ afterEach(() => { state.restore(); });
+ });
+
+ describe('cleanHTML', () => {
+ const dirtyText = `&${Object.keys(alphanumericMap).join('; , &')};`;
+ const cleanText = `${Object.values(alphanumericMap).join(' , ')}`;
+
+ it('escapes alphanumerics and sets them to be literals', () => {
+ expect(module.hooks.cleanHTML({ initialText: dirtyText })).toEqual(cleanText);
+ });
+ });
+
+ describe('escapeHTMLSpecialChars', () => {
+ const cleanText = `${Object.values(alphanumericMap).join(' , ')}`;
+
+ const mockDispatch = jest.fn((args) => ({ mockDispatch: args }));
+
+ const ref = {
+ current: {
+ dispatch: mockDispatch,
+ state: {
+ doc: {
+ toString: () => cleanText,
+ },
+ },
+ },
+ };
+ const mockHideBtn = jest.fn();
+ it('unescapes literals and sets them to be alphanumerics', () => {
+ module.hooks.escapeHTMLSpecialChars({ ref, hideBtn: mockHideBtn });
+ expect(mockDispatch).toHaveBeenCalled();
+ expect(mockHideBtn).toHaveBeenCalled();
+ });
+ });
+
describe('createCodeMirrorDomNode', () => {
const props = {
ref: {
@@ -60,9 +110,11 @@ describe('CodeEditor', () => {
});
describe('Component', () => {
describe('Snapshots', () => {
+ const mockHideBtn = jest.fn().mockName('mockHidebtn');
let props;
beforeAll(() => {
props = {
+ intl: { formatMessage },
innerRef: {
current: 'sOmEvALUE',
},
@@ -74,6 +126,7 @@ describe('CodeEditor', () => {
jest.clearAllMocks();
});
test('Renders and calls Hooks ', () => {
+ jest.spyOn(module.hooks, 'prepareShowBtnEscapeHTML').mockImplementation(() => ({ showBtnEscapeHTML: true, hideBtn: mockHideBtn }));
// Note: ref won't show up as it is not acutaly a DOM attribute.
expect(shallow()).toMatchSnapshot();
expect(module.hooks.createCodeMirrorDomNode).toHaveBeenCalled();
diff --git a/src/editors/containers/TextEditor/components/CodeEditor/messages.js b/src/editors/containers/TextEditor/components/CodeEditor/messages.js
new file mode 100644
index 000000000..9df01970a
--- /dev/null
+++ b/src/editors/containers/TextEditor/components/CodeEditor/messages.js
@@ -0,0 +1,9 @@
+export const messages = {
+ escapeHTMLButtonLabel: {
+ id: 'authoring.texteditor.codeEditor.escapeHTMLButton',
+ defaultMessage: 'Unescape HTML Literals',
+ description: 'Label For escape special html charectars button',
+ },
+};
+
+export default messages;
diff --git a/src/editors/containers/TextEditor/components/RawEditor/__snapshots__/index.test.jsx.snap b/src/editors/containers/TextEditor/components/RawEditor/__snapshots__/index.test.jsx.snap
index d049d36f1..32899800f 100644
--- a/src/editors/containers/TextEditor/components/RawEditor/__snapshots__/index.test.jsx.snap
+++ b/src/editors/containers/TextEditor/components/RawEditor/__snapshots__/index.test.jsx.snap
@@ -14,7 +14,7 @@ exports[`RawEditor renders as expected with default behavior 1`] = `
>
You are using the raw HTML editor.
-
-