Skip to content

Commit

Permalink
refactor: refactor md editor
Browse files Browse the repository at this point in the history
  • Loading branch information
yuanyxh committed Aug 25, 2024
1 parent b297595 commit d65404e
Show file tree
Hide file tree
Showing 8 changed files with 1,131 additions and 1,390 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = {
browser: true,
es2015: true
},
ignorePatterns: ['*.min.js'],
extends: [
'plugin:mdx/recommended',
'eslint:recommended',
Expand Down Expand Up @@ -54,6 +55,7 @@ module.exports = {
['^@/coder'],
['^@/markdowns'],
['^@/examples'],
['^@/tools'],
['^@/components'],
['^@/App'],
['^@/assets'],
Expand Down
14 changes: 2 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,9 @@
},
"dependencies": {
"@ant-design/icons": "^5.3.7",
"@milkdown/core": "^7.3.6",
"@milkdown/plugin-diagram": "^7.3.6",
"@milkdown/plugin-history": "^7.3.6",
"@milkdown/plugin-indent": "^7.3.6",
"@milkdown/plugin-listener": "^7.3.6",
"@milkdown/plugin-prism": "^7.3.6",
"@milkdown/plugin-upload": "^7.3.6",
"@milkdown/preset-commonmark": "^7.3.6",
"@milkdown/preset-gfm": "^7.3.6",
"@milkdown/utils": "^7.3.6",
"@monaco-editor/react": "^4.6.0",
"antd": "^5.17.2",
"classnames": "^2.5.1",
"hypermd": "^0.3.11",
"lodash-es": "^4.17.21",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
Expand Down Expand Up @@ -105,4 +95,4 @@
"[email protected]": "patches/[email protected]"
}
}
}
}
1,961 changes: 1,058 additions & 903 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

10 changes: 2 additions & 8 deletions src/App.less
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ table {

// sustom scrollbar
::-webkit-scrollbar {
width: 12px;
height: 8px;
width: 6px;
height: 5px;
}

::-webkit-scrollbar-thumb {
Expand All @@ -121,12 +121,6 @@ table {
border-radius: var(--border-radius-base);
}

@media screen and (width <= 1366px) {
::-webkit-scrollbar {
width: 6px;
}
}

// use canonical syntax when ::-webkit-scrollbar is not supported
@supports not selector(::-webkit-scrollbar) {
html,
Expand Down
201 changes: 63 additions & 138 deletions src/filehandle/md_editor/component/MDEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,18 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import type { FH } from '@/filehandle/utils/fileManager';

import styles from './styles/MDEditor.module.less';
import '@/assets/styles/prism-one-dark.css';
import type { UploadInfo } from '../store/useMDStore';
import { useMDStore } from '../store/useMDStore';
import { reduce } from '../theme-reduce';
import { toFormData } from '../utils';

import { defaultValueCtx, Editor, rootCtx } from '@milkdown/core';
// import { clipboard } from '@milkdown/plugin-clipboard';
import { diagram } from '@milkdown/plugin-diagram';
import { history } from '@milkdown/plugin-history';
import { indent } from '@milkdown/plugin-indent';
import { listener, listenerCtx } from '@milkdown/plugin-listener';
import { prism, prismConfig } from '@milkdown/plugin-prism';
import type { Uploader } from '@milkdown/plugin-upload';
import { upload, uploadConfig } from '@milkdown/plugin-upload';
import {
blockquoteAttr,
bulletListAttr,
codeBlockAttr,
commonmark,
emphasisAttr,
headingAttr,
hrAttr,
imageAttr,
inlineCodeAttr,
insertImageCommand,
insertImageInputRule,
linkAttr,
orderedListAttr,
paragraphAttr
} from '@milkdown/preset-commonmark';
import { gfm } from '@milkdown/preset-gfm';
import { getMarkdown } from '@milkdown/utils';
import '../utils/codemirror.min';
import { fromTextArea } from 'hypermd';

interface HandlerAction {
setPlaceholder<T extends HTMLElement>(placeholder: T): void;
resize(): void;
finish(text: string, cursor?: number): void;
}

interface IMDEditorProps {
currentHandle: FH | null;
Expand All @@ -44,147 +23,73 @@ interface IMDEditorProps {
onSave(markdown: string): any;
}

type Editor = ReturnType<typeof fromTextArea>;

export interface IMDEditorExpose {
getMarkdown(): string;
}

const blockElementKeys = [
blockquoteAttr.key,
bulletListAttr.key,
orderedListAttr.key,
headingAttr.key,
hrAttr.key,
imageAttr.key,
paragraphAttr.key
];

const blockClass = { class: styles.typography };

const isMac = (() => {
const agent = navigator.userAgent.toLowerCase();
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
const _isMac = /macintosh|mac os x/i.test(navigator.userAgent);
if (agent.indexOf('win32') >= 0 || agent.indexOf('wow32') >= 0) {
return false;
}
if (agent.indexOf('win64') >= 0 || agent.indexOf('wow64') >= 0) {
return false;
}
if (isMac) {
if (_isMac) {
return true;
}

return false;
})();

const getMDString = getMarkdown();
const createMDImage = (url: string) => `![${Date.now()}](${url})`;

let uploadInfo: UploadInfo | null = null;
const selfUpload = async (file: File) => {
const fileHandler = (files: FileList, handler: HandlerAction) => {
if (!uploadInfo || uploadInfo.url.trim() === '') {
return window.URL.createObjectURL(file);
// TODO: to base64
return handler.finish(createMDImage(window.URL.createObjectURL(files[0])));
}

const body = uploadInfo.body.trim() ? JSON.parse(uploadInfo.body) : {};
const data = toFormData({ ...body, [uploadInfo.field]: file });
const data = toFormData({ ...body, [uploadInfo.field]: files[0] });

const navigation = uploadInfo.navigation;

return fetch(uploadInfo.url, {
fetch(uploadInfo.url, {
method: 'POST',
body: data
})
.then((res) => res.json())
.then((value) => navigation.split('.').reduce((prev, curr) => prev[curr], value));
};

const uploader: Uploader = async (files, schema) => {
const images: File[] = [];

for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (!file) {
continue;
}

if (!file.type.includes('image')) {
continue;
}

images.push(file);
}
.then((value) => {
const url = navigation.split('.').reduce((prev, curr) => prev[curr], value);

const nodes = await Promise.all(
images.map(async (image) => {
const src = await selfUpload(image);
const alt = image.name;
return schema.nodes.image.createAndFill({
src,
alt
})!;
handler.finish(createMDImage(url));
})
);
.catch(() => {
handler.finish('');
});

return nodes;
return true;
};

function createMDEditor(el: HTMLElement, value = '', onUpdate: (md: string) => void) {
return (
Editor.make()
.config((ctx) => {
ctx.set(rootCtx, el);
ctx.set(defaultValueCtx, value);

ctx.update(uploadConfig.key, (prev) => ({
...prev,
uploader
}));

ctx.set(prismConfig.key, {
configureRefractor: (r) => {
r.alias('shell', 'sh');
}
});

blockElementKeys.forEach((key) => ctx.set(key, () => blockClass));

ctx.set(inlineCodeAttr.key, () => ({ class: styles.inlineCode }));
ctx.set(linkAttr.key, () => ({ rel: 'noopener noreferrer' }));
ctx.set(codeBlockAttr.key, () => ({ pre: blockClass, code: {} }));
ctx.set(emphasisAttr.key, () => blockClass);

ctx.get(listenerCtx).markdownUpdated((_ctx, md) => onUpdate(md));
})
.config(reduce)
.use(commonmark)
.use(prism)
.use(insertImageInputRule)
.use(insertImageCommand)
.use(gfm)
.use(history)
.use(diagram)
// FIXME: There is a problem with this plug -in,
// and the code block is automatically added when the text is pasted
// .use(clipboard)
.use(indent)
.use(upload)
.use(listener)
.create()
);
}

const MDEditor = forwardRef<IMDEditorExpose, IMDEditorProps>(function MDEditor(props, ref) {
const { currentHandle, changed, onChanged, onSave } = props;

const editorContainerRef = useRef<HTMLDivElement>(null);
const editorContainerRef = useRef<HTMLTextAreaElement>(null);
const editorRef = useRef<Editor>();
const mdStringRef = useRef('');
const latestHandleRef = useRef<FH | null>();
const isFirstRender = useRef(true);

uploadInfo = useMDStore().uploadInfo;

useImperativeHandle(ref, () => ({
getMarkdown() {
return editorRef.current ? getMDString(editorRef.current.ctx) : '';
return editorRef.current ? editorRef.current.getValue() : '';
}
}));

Expand All @@ -195,30 +100,49 @@ const MDEditor = forwardRef<IMDEditorExpose, IMDEditorProps>(function MDEditor(p
?.getFile()
.then((file) => file.text())
.then((markdown) => {
createMDEditor(editorContainerRef.current!, markdown, onUpdate).then((value) => {
if (latestHandleRef.current && latestHandleRef.current !== currentHandle) {
return value.destroy(true);
}
if (latestHandleRef.current && latestHandleRef.current !== currentHandle) {
return void 0;
}

editorRef.current = fromTextArea(editorContainerRef.current!, {
mode: {
name: 'hypermd',
hashtag: false
},
hmdFold: {
image: true,
link: true,
math: true,
html: true,
emoji: true
},
lineNumbers: false,
gutters: false
});

editorRef.current && editorRef.current.destroy(true);
isFirstRender.current = true;

editorRef.current = value;
mdStringRef.current = markdown;
onChanged(false);
});
editorRef.current.setOption('hmdInsertFile', fileHandler);
editorRef.current.on('change', onUpdate);

editorRef.current.setValue(markdown);
});

return () => {
editorRef.current?.destroy(true);
editorRef.current?.toTextArea();
};
}, [currentHandle]);

function onUpdate(md: string) {
function onUpdate(cm: Editor) {
if (isFirstRender.current) {
return (isFirstRender.current = false);
}

onChanged(true);
mdStringRef.current = md;
mdStringRef.current = cm.getValue();
}

const handleSave = (e: React.KeyboardEvent<HTMLDivElement>) => {
const handleSave = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (isMac) {
if (e.metaKey && e.key.toLocaleLowerCase() === 's') {
e.preventDefault();
Expand All @@ -235,12 +159,13 @@ const MDEditor = forwardRef<IMDEditorExpose, IMDEditorProps>(function MDEditor(p
};

return (
<div
<textarea
ref={editorContainerRef}
id="md-editor"
style={{ display: 'none' }}
className={styles.editorContainer}
onKeyDown={handleSave}
></div>
></textarea>
);
});

Expand Down
Loading

0 comments on commit d65404e

Please sign in to comment.