Skip to content

Commit 91d73bf

Browse files
committed
feat: add common highlight entry #704
#704
1 parent 4e6cda0 commit 91d73bf

9 files changed

Lines changed: 535 additions & 1 deletion

File tree

core/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,36 @@ export default function App() {
256256
}
257257
```
258258

259+
### Common Code Highlight
260+
261+
Use `@uiw/react-md-editor/common` to keep syntax highlighting while only including the `rehype-prism-plus/common` language subset. This is a middle ground between the default full-language build and `nohighlight`.
262+
263+
```jsx mdx:preview
264+
import React from "react";
265+
import MDEditor from '@uiw/react-md-editor/common';
266+
267+
const code = `**Hello world!!!**
268+
\`\`\`js
269+
function greet(name) {
270+
console.log('hello', name);
271+
}
272+
\`\`\`
273+
`
274+
275+
export default function App() {
276+
const [value, setValue] = React.useState(code);
277+
return (
278+
<div className="container">
279+
<MDEditor
280+
value={value}
281+
onChange={setValue}
282+
/>
283+
<MDEditor.Markdown source={value} style={{ whiteSpace: 'pre-wrap' }} />
284+
</div>
285+
);
286+
}
287+
```
288+
259289
### Placeholder & maxLength
260290

261291
"Below is an example that sets the `placeholder` for the editor and defines the maximum input character length as `10` characters."

core/common.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
declare module '@uiw/react-md-editor/common' {
2+
import MDEditor from '@uiw/react-md-editor/esm/Editor.common';
3+
import * as commands from '@uiw/react-md-editor/esm/commands';
4+
import * as MarkdownUtil from '@uiw/react-md-editor/esm/utils/markdownUtils';
5+
export { headingExecute } from '@uiw/react-md-editor/esm/commands/title';
6+
export * from '@uiw/react-md-editor/esm/commands';
7+
export * from '@uiw/react-md-editor/esm/commands/group';
8+
export * from '@uiw/react-md-editor/esm/utils/markdownUtils';
9+
export * from '@uiw/react-md-editor/esm/utils/InsertTextAtPosition';
10+
export * from '@uiw/react-md-editor/esm/Editor.common';
11+
export * from '@uiw/react-md-editor/esm/Context';
12+
export * from '@uiw/react-md-editor/esm/Types';
13+
export { default as handleKeyDown } from '@uiw/react-md-editor/esm/components/TextArea/handleKeyDown';
14+
export { default as shortcuts } from '@uiw/react-md-editor/esm/components/TextArea/shortcuts';
15+
export { MarkdownUtil, commands };
16+
export default MDEditor;
17+
}

core/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
"types": "./lib/index.d.ts",
1717
"require": "./lib/index.js"
1818
},
19+
"./common": {
20+
"import": "./esm/index.common.js",
21+
"types": "./lib/index.common.d.ts",
22+
"require": "./lib/index.common.js"
23+
},
1924
"./nohighlight": {
2025
"import": "./esm/index.nohighlight.js",
2126
"types": "./lib/index.nohighlight.d.ts",
@@ -53,6 +58,7 @@
5358
},
5459
"files": [
5560
"markdown-editor.css",
61+
"common.d.ts",
5662
"nohighlight.d.ts",
5763
"commands.d.ts",
5864
"commands-cn.d.ts",
@@ -68,7 +74,7 @@
6874
},
6975
"dependencies": {
7076
"@babel/runtime": "^7.14.6",
71-
"@uiw/react-markdown-preview": "^5.0.6",
77+
"@uiw/react-markdown-preview": "^5.2.0",
7278
"rehype": "~13.0.0",
7379
"rehype-prism-plus": "~2.0.0"
7480
},

core/src/Editor.common.tsx

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import React, { useEffect, useReducer, useMemo, useRef, useImperativeHandle } from 'react';
2+
import MarkdownPreview from '@uiw/react-markdown-preview/common';
3+
import { ToolbarVisibility } from './components/Toolbar/';
4+
import TextArea from './components/TextArea/index.common';
5+
import DragBar from './components/DragBar/';
6+
import { getCommands, getExtraCommands, type ICommand, type TextState, TextAreaCommandOrchestrator } from './commands/';
7+
import { reducer, EditorContext, type ContextStore } from './Context';
8+
import type { MDEditorProps } from './Types';
9+
10+
function setGroupPopFalse(data: Record<string, boolean> = {}) {
11+
Object.keys(data).forEach((keyname) => {
12+
data[keyname] = false;
13+
});
14+
return data;
15+
}
16+
17+
export interface RefMDEditor extends ContextStore {}
18+
19+
const InternalMDEditor = React.forwardRef<RefMDEditor, MDEditorProps>(
20+
(props: MDEditorProps, ref: React.ForwardedRef<RefMDEditor>) => {
21+
const {
22+
prefixCls = 'w-md-editor',
23+
className,
24+
value: propsValue,
25+
commands = getCommands(),
26+
commandsFilter,
27+
direction,
28+
extraCommands = getExtraCommands(),
29+
height = 200,
30+
enableScroll = true,
31+
visibleDragbar = typeof props.visiableDragbar === 'boolean' ? props.visiableDragbar : true,
32+
highlightEnable = true,
33+
preview: previewType = 'live',
34+
fullscreen = false,
35+
overflow = true,
36+
previewOptions = {},
37+
textareaProps,
38+
maxHeight = 1200,
39+
minHeight = 100,
40+
autoFocus,
41+
autoFocusEnd = false,
42+
tabSize = 2,
43+
defaultTabEnable = false,
44+
onChange,
45+
onStatistics,
46+
onHeightChange,
47+
hideToolbar,
48+
toolbarBottom = false,
49+
components,
50+
renderTextarea,
51+
...other
52+
} = props || {};
53+
const cmds = commands
54+
.map((item) => (commandsFilter ? commandsFilter(item, false) : item))
55+
.filter(Boolean) as ICommand[];
56+
const extraCmds = extraCommands
57+
.map((item) => (commandsFilter ? commandsFilter(item, true) : item))
58+
.filter(Boolean) as ICommand[];
59+
let [state, dispatch] = useReducer(reducer, {
60+
markdown: propsValue,
61+
preview: previewType,
62+
components,
63+
height,
64+
minHeight,
65+
highlightEnable,
66+
tabSize,
67+
defaultTabEnable,
68+
scrollTop: 0,
69+
scrollTopPreview: 0,
70+
commands: cmds,
71+
extraCommands: extraCmds,
72+
fullscreen,
73+
barPopup: {},
74+
});
75+
const container = useRef<HTMLDivElement>(null);
76+
const previewRef = useRef<HTMLDivElement>(null);
77+
const enableScrollRef = useRef(enableScroll);
78+
79+
useImperativeHandle(ref, () => ({ ...state, container: container.current, dispatch }));
80+
useMemo(() => (enableScrollRef.current = enableScroll), [enableScroll]);
81+
useEffect(() => {
82+
const stateInit: ContextStore = {};
83+
if (container.current) {
84+
stateInit.container = container.current || undefined;
85+
}
86+
stateInit.markdown = propsValue || '';
87+
stateInit.barPopup = {};
88+
if (dispatch) {
89+
dispatch({ ...state, ...stateInit });
90+
}
91+
// eslint-disable-next-line react-hooks/exhaustive-deps
92+
}, []);
93+
94+
const cls = [
95+
className,
96+
'wmde-markdown-var',
97+
direction ? `${prefixCls}-${direction}` : null,
98+
prefixCls,
99+
state.preview ? `${prefixCls}-show-${state.preview}` : null,
100+
state.fullscreen ? `${prefixCls}-fullscreen` : null,
101+
]
102+
.filter(Boolean)
103+
.join(' ')
104+
.trim();
105+
106+
useMemo(
107+
() => propsValue !== state.markdown && dispatch({ markdown: propsValue || '' }),
108+
[propsValue, state.markdown],
109+
);
110+
// eslint-disable-next-line react-hooks/exhaustive-deps
111+
useMemo(() => previewType !== state.preview && dispatch({ preview: previewType }), [previewType]);
112+
// eslint-disable-next-line react-hooks/exhaustive-deps
113+
useMemo(() => tabSize !== state.tabSize && dispatch({ tabSize }), [tabSize]);
114+
useMemo(
115+
() => highlightEnable !== state.highlightEnable && dispatch({ highlightEnable }),
116+
// eslint-disable-next-line react-hooks/exhaustive-deps
117+
[highlightEnable],
118+
);
119+
// eslint-disable-next-line react-hooks/exhaustive-deps
120+
useMemo(() => autoFocus !== state.autoFocus && dispatch({ autoFocus: autoFocus }), [autoFocus]);
121+
useMemo(() => autoFocusEnd !== state.autoFocusEnd && dispatch({ autoFocusEnd: autoFocusEnd }), [autoFocusEnd]);
122+
useMemo(
123+
() => fullscreen !== state.fullscreen && dispatch({ fullscreen: fullscreen }),
124+
// eslint-disable-next-line react-hooks/exhaustive-deps
125+
[fullscreen],
126+
);
127+
// eslint-disable-next-line react-hooks/exhaustive-deps
128+
useMemo(() => height !== state.height && dispatch({ height: height }), [height]);
129+
useMemo(
130+
() => height !== state.height && onHeightChange && onHeightChange(state.height, height, state),
131+
[height, onHeightChange, state],
132+
);
133+
// eslint-disable-next-line react-hooks/exhaustive-deps
134+
useMemo(() => commands !== state.commands && dispatch({ commands: cmds }), [props.commands]);
135+
// eslint-disable-next-line react-hooks/exhaustive-deps
136+
useMemo(
137+
() => extraCommands !== state.extraCommands && dispatch({ extraCommands: extraCmds }),
138+
[props.extraCommands],
139+
);
140+
141+
const textareaDomRef = useRef<HTMLDivElement>();
142+
const active = useRef<'text' | 'preview'>('preview');
143+
const initScroll = useRef(false);
144+
145+
useMemo(() => {
146+
textareaDomRef.current = state.textareaWarp;
147+
if (state.textareaWarp) {
148+
state.textareaWarp.addEventListener('mouseover', () => {
149+
active.current = 'text';
150+
});
151+
state.textareaWarp.addEventListener('mouseleave', () => {
152+
active.current = 'preview';
153+
});
154+
}
155+
}, [state.textareaWarp]);
156+
157+
const handleScroll = (e: React.UIEvent<HTMLDivElement>, type: 'text' | 'preview') => {
158+
if (!enableScrollRef.current) return;
159+
const textareaDom = textareaDomRef.current;
160+
const previewDom = previewRef.current ? previewRef.current : undefined;
161+
if (!initScroll.current) {
162+
active.current = type;
163+
initScroll.current = true;
164+
}
165+
if (textareaDom && previewDom) {
166+
const scale =
167+
(textareaDom.scrollHeight - textareaDom.offsetHeight) / (previewDom.scrollHeight - previewDom.offsetHeight);
168+
if (e.target === textareaDom && active.current === 'text') {
169+
previewDom.scrollTop = textareaDom.scrollTop / scale;
170+
}
171+
if (e.target === previewDom && active.current === 'preview') {
172+
textareaDom.scrollTop = previewDom.scrollTop * scale;
173+
}
174+
let scrollTop = 0;
175+
if (active.current === 'text') {
176+
scrollTop = textareaDom.scrollTop || 0;
177+
} else if (active.current === 'preview') {
178+
scrollTop = previewDom.scrollTop || 0;
179+
}
180+
dispatch({ scrollTop });
181+
}
182+
};
183+
184+
const previewClassName = `${prefixCls}-preview ${previewOptions.className || ''}`;
185+
const handlePreviewScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => handleScroll(e, 'preview');
186+
let mdPreview = useMemo(
187+
() => (
188+
<div ref={previewRef} className={previewClassName}>
189+
<MarkdownPreview {...previewOptions} onScroll={handlePreviewScroll} source={state.markdown || ''} />
190+
</div>
191+
),
192+
[previewClassName, previewOptions, state.markdown],
193+
);
194+
const preview = components?.preview && components?.preview(state.markdown || '', state, dispatch);
195+
if (preview && React.isValidElement(preview)) {
196+
mdPreview = (
197+
<div className={previewClassName} ref={previewRef} onScroll={handlePreviewScroll}>
198+
{preview}
199+
</div>
200+
);
201+
}
202+
203+
const containerStyle = { ...other.style, height: state.height || '100%' };
204+
const containerClick = () => dispatch({ barPopup: { ...setGroupPopFalse(state.barPopup) } });
205+
const dragBarChange = (newHeight: number) => dispatch({ height: newHeight });
206+
207+
const changeHandle = (evn: React.ChangeEvent<HTMLTextAreaElement>) => {
208+
onChange && onChange(evn.target.value, evn, state);
209+
if (textareaProps && textareaProps.onChange) {
210+
textareaProps.onChange(evn);
211+
}
212+
if (state.textarea && state.textarea instanceof HTMLTextAreaElement && onStatistics) {
213+
const obj = new TextAreaCommandOrchestrator(state.textarea!);
214+
const objState = (obj.getState() || {}) as TextState;
215+
onStatistics({
216+
...objState,
217+
lineCount: evn.target.value.split('\n').length,
218+
length: evn.target.value.length,
219+
});
220+
}
221+
};
222+
return (
223+
<EditorContext.Provider value={{ ...state, dispatch }}>
224+
<div ref={container} className={cls} {...other} onClick={containerClick} style={containerStyle}>
225+
<ToolbarVisibility
226+
hideToolbar={hideToolbar}
227+
toolbarBottom={toolbarBottom}
228+
prefixCls={prefixCls}
229+
overflow={overflow}
230+
placement="top"
231+
/>
232+
<div className={`${prefixCls}-content`}>
233+
{/(edit|live)/.test(state.preview || '') && (
234+
<TextArea
235+
className={`${prefixCls}-input`}
236+
prefixCls={prefixCls}
237+
autoFocus={autoFocus}
238+
{...textareaProps}
239+
onChange={changeHandle}
240+
renderTextarea={components?.textarea || renderTextarea}
241+
onScroll={(e) => handleScroll(e, 'text')}
242+
/>
243+
)}
244+
{/(live|preview)/.test(state.preview || '') && mdPreview}
245+
</div>
246+
{visibleDragbar && !state.fullscreen && (
247+
<DragBar
248+
prefixCls={prefixCls}
249+
height={state.height as number}
250+
maxHeight={maxHeight!}
251+
minHeight={minHeight!}
252+
onChange={dragBarChange}
253+
/>
254+
)}
255+
<ToolbarVisibility
256+
hideToolbar={hideToolbar}
257+
toolbarBottom={toolbarBottom}
258+
prefixCls={prefixCls}
259+
overflow={overflow}
260+
placement="bottom"
261+
/>
262+
</div>
263+
</EditorContext.Provider>
264+
);
265+
},
266+
);
267+
268+
type EditorComponent = typeof InternalMDEditor & {
269+
Markdown: typeof MarkdownPreview;
270+
};
271+
272+
const Editor = InternalMDEditor as EditorComponent;
273+
Editor.Markdown = MarkdownPreview;
274+
Editor.displayName = 'MDEditor';
275+
276+
export default Editor;

0 commit comments

Comments
 (0)