feat: align files with commit 5d52a28 and f9dff0 (#210)

This PR aims to fix the commit mistakes I made when trying to merge with a refactored fork. This will keep the changes I made in the refactor.
This commit is contained in:
Raymond Zhou
2023-01-24 06:43:01 -08:00
committed by GitHub
parent acee24eaa7
commit f135bd2b4a
153 changed files with 4757 additions and 4075 deletions

View File

@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CodeEditor Component Snapshots Renders and calls Hooks 1`] = `
<div>
<div
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>
`;

View File

@@ -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;

View File

@@ -0,0 +1,71 @@
import React, { useEffect } from 'react';
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 alphanumericMap from './constants';
import './index.scss';
const CODEMIRROR_LANGUAGES = { HTML: 'html', XML: 'xml' };
export const state = {
showBtnEscapeHTML: (val) => React.useState(val),
};
export const prepareShowBtnEscapeHTML = () => {
const [visibility, setVisibility] = state.showBtnEscapeHTML(true);
const hide = () => setVisibility(false);
return { showBtnEscapeHTML: visibility, hideBtn: hide };
};
export const cleanHTML = ({ initialText }) => {
const translateRegex = new RegExp(`&(${Object.keys(alphanumericMap).join('|')});`, 'g');
const translator = ($0, $1) => alphanumericMap[$1];
return initialText.replace(translateRegex, translator);
};
export const createCodeMirrorDomNode = ({
ref,
initialText,
upstreamRef,
lang,
}) => {
useEffect(() => {
const languageExtension = lang === CODEMIRROR_LANGUAGES.HTML ? html() : xml();
const cleanText = cleanHTML({ initialText });
const newState = EditorState.create({
doc: cleanText,
extensions: [basicSetup, languageExtension, EditorView.lineWrapping],
});
const view = new EditorView({ state: newState, parent: ref.current });
// eslint-disable-next-line no-param-reassign
upstreamRef.current = view;
view.focus();
return () => {
// called on cleanup
view.destroy();
};
}, []);
};
export const 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();
};

View File

@@ -0,0 +1,55 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import {
Button,
} from '@edx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
import './index.scss';
import * as hooks from './hooks';
export const CodeEditor = ({
innerRef,
value,
lang,
// injected
intl,
}) => {
const DOMref = useRef();
const btnRef = useRef();
hooks.createCodeMirrorDomNode({
ref: DOMref, initialText: value, upstreamRef: innerRef, lang,
});
const { showBtnEscapeHTML, hideBtn } = hooks.prepareShowBtnEscapeHTML();
return (
<div>
<div id="CodeMirror" ref={DOMref} />
{showBtnEscapeHTML && (
<Button
variant="tertiary"
aria-label={intl.formatMessage(messages.escapeHTMLButtonLabel)}
ref={btnRef}
onClick={() => hooks.escapeHTMLSpecialChars({ ref: innerRef, hideBtn })}
>
<FormattedMessage {...messages.escapeHTMLButtonLabel} />
</Button>
)}
</div>
);
};
CodeEditor.propTypes = {
innerRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.any }),
]).isRequired,
value: PropTypes.string.isRequired,
intl: intlShape.isRequired,
lang: PropTypes.string.isRequired,
};
export default injectIntl(CodeEditor);

View File

@@ -0,0 +1,2 @@
.cm-editor { height: 100% }
.cm-scroller { overflow: auto }

View File

@@ -0,0 +1,143 @@
import React from 'react';
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';
import * as hooks from './hooks';
jest.mock('@codemirror/view');
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn(val => ({ current: val })),
useEffect: jest.fn(),
useCallback: (cb, prereqs) => ({ cb, prereqs }),
}));
jest.mock('@codemirror/state', () => ({
...jest.requireActual('@codemirror/state'),
EditorState: {
create: jest.fn(),
},
}));
jest.mock('@codemirror/lang-html', () => ({
html: jest.fn(),
}));
jest.mock('@codemirror/lang-xml', () => ({
xml: jest.fn(),
}));
jest.mock('codemirror', () => ({
basicSetup: 'bAsiCSetUp',
}));
const state = new MockUseState(hooks);
describe('CodeEditor', () => {
describe('Hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
state.testGetter(state.keys.showBtnEscapeHTML);
describe('stateHooks', () => {
beforeEach(() => {
state.mock();
});
it('prepareShowBtnEscapeHTML', () => {
const hook = 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(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', () => {
hooks.escapeHTMLSpecialChars({ ref, hideBtn: mockHideBtn });
expect(mockDispatch).toHaveBeenCalled();
expect(mockHideBtn).toHaveBeenCalled();
});
});
describe('createCodeMirrorDomNode', () => {
const props = {
ref: {
current: 'sOmEvAlUe',
},
lang: 'html',
initialText: 'sOmEhTmL',
upstreamRef: {
current: 'sOmEotHERvAlUe',
},
};
beforeEach(() => {
hooks.createCodeMirrorDomNode(props);
});
it('calls useEffect and sets up codemirror objects', () => {
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toStrictEqual([]);
cb();
expect(EditorState.create).toHaveBeenCalled();
expect(EditorView).toHaveBeenCalled();
expect(html).toHaveBeenCalled();
});
});
});
describe('Component', () => {
describe('Snapshots', () => {
const mockHideBtn = jest.fn().mockName('mockHidebtn');
let props;
beforeAll(() => {
props = {
intl: { formatMessage },
innerRef: {
current: 'sOmEvALUE',
},
lang: 'html',
value: 'mOcKhTmL',
};
jest.spyOn(hooks, 'createCodeMirrorDomNode').mockImplementation(() => ({}));
});
afterAll(() => {
jest.clearAllMocks();
});
test('Renders and calls Hooks ', () => {
jest.spyOn(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(hooks.createCodeMirrorDomNode).toHaveBeenCalled();
});
});
});
});

View File

@@ -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;

View File

@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RawEditor renders as expected with default behavior 1`] = `
<div>
<Alert
variant="danger"
>
You are using the raw
html
editor.
</Alert>
<injectIntl(ShimmedIntlComponent)
innerRef={
Object {
"current": Object {
"value": "Ref Value",
},
}
}
lang="html"
value="eDiTablE Text"
/>
</div>
`;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@edx/paragon';
import CodeEditor from '../CodeEditor';
function getValue(content) {
if (!content) { return null; }
if (typeof content === 'string') { return content; }
return content.data?.data;
}
export const RawEditor = ({
editorRef,
content,
lang,
}) => {
const value = getValue(content);
return (
<div>
<Alert variant="danger">
You are using the raw {lang} editor.
</Alert>
{ value ? (
<CodeEditor
innerRef={editorRef}
value={value}
lang={lang}
/>
) : null}
</div>
);
};
RawEditor.defaultProps = {
editorRef: null,
content: null,
lang: 'html',
};
RawEditor.propTypes = {
editorRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.any }),
]),
content: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
data: PropTypes.shape({ data: PropTypes.string }),
}),
]),
lang: PropTypes.string,
};
export default RawEditor;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { shallow } from 'enzyme';
import { RawEditor } from '.';
describe('RawEditor', () => {
const props = {
editorRef: {
current: {
value: 'Ref Value',
},
},
content: { data: { data: 'eDiTablE Text' } },
};
test('renders as expected with default behavior', () => {
expect(shallow(<RawEditor {...props} />)).toMatchSnapshot();
});
});