feat: unescape alphanumerics in code Editor (#112)
* feat: unescape alphanumerics in code Editor * fix: replace disable with not render
This commit is contained in:
@@ -3,7 +3,18 @@
|
||||
exports[`CodeEditor Component Snapshots Renders and calls Hooks 1`] = `
|
||||
<div>
|
||||
<div
|
||||
id="editor"
|
||||
id="CodeMirror"
|
||||
/>
|
||||
<Button
|
||||
aria-label="Unescape HTML Literals"
|
||||
onClick={[Function]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Unescape HTML Literals"
|
||||
description="Label For escape special html charectars button"
|
||||
id="authoring.texteditor.codeEditor.escapeHTMLButton"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<div>
|
||||
<div id="editor" ref={DOMref} />
|
||||
<div id="CodeMirror" ref={DOMref} />
|
||||
{showBtnEscapeHTML && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
aria-label={intl.formatMessage(messages.escapeHTMLButtonLabel)}
|
||||
ref={btnRef}
|
||||
onClick={() => module.hooks.escapeHTMLSpecialChars({ ref: innerRef, hideBtn })}
|
||||
>
|
||||
<FormattedMessage {...messages.escapeHTMLButtonLabel} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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(<module.CodeEditor {...props} />)).toMatchSnapshot();
|
||||
expect(module.hooks.createCodeMirrorDomNode).toHaveBeenCalled();
|
||||
|
||||
@@ -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;
|
||||
@@ -14,7 +14,7 @@ exports[`RawEditor renders as expected with default behavior 1`] = `
|
||||
>
|
||||
You are using the raw HTML editor.
|
||||
</Alert>
|
||||
<CodeEditor
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
innerRef={
|
||||
Object {
|
||||
"current": Object {
|
||||
|
||||
@@ -36,7 +36,7 @@ exports[`SourceCodeModal renders as expected with default behavior 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<CodeEditor
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
innerRef="moCKrEf"
|
||||
value="mOckHtMl"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user