blob: 759033ccaf2ebf67b503cfbe4db6477e46e7f24d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useEffect, useState } from 'react';
import { minimalSetup } from 'codemirror';
import { EditorState, Compartment } from '@codemirror/state';
import { EditorView, placeholder } from '@codemirror/view';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { languages } from '@codemirror/language-data';
import { Editor } from '../types';
import { isDarkTheme } from '@/utils/common';
import createEditorUtils from './extension';
const editableCompartment = new Compartment();
export function htmlRender(el: HTMLElement | null) {
if (!el) return;
// Replace all br tags with newlines
// Fixed an issue where the BR tag in the editor block formula HTML caused rendering errors.
el.querySelectorAll('p').forEach((p) => {
if (p.innerHTML.startsWith('$$') && p.innerHTML.endsWith('$$')) {
const str = p.innerHTML.replace(/<br>/g, '\n');
p.innerHTML = str;
}
});
// change table style
el.querySelectorAll('table').forEach((table) => {
if (
(table.parentNode as HTMLDivElement)?.classList.contains(
'table-responsive',
)
) {
return;
}
table.classList.add('table', 'table-bordered');
const div = document.createElement('div');
div.className = 'table-responsive';
table.parentNode?.replaceChild(div, table);
div.appendChild(table);
});
// add rel nofollow for link not includes domain
el.querySelectorAll('a').forEach((a) => {
const base = window.location.origin;
const targetUrl = new URL(a.href, base);
if (targetUrl.origin !== base) {
a.rel = 'nofollow';
}
});
}
export const useEditor = ({
editorRef,
placeholder: placeholderText,
autoFocus,
onChange,
onFocus,
onBlur,
}) => {
const [editor, setEditor] = useState<Editor | null>(null);
const [value, setValue] = useState<string>('');
const init = async () => {
const isDark = isDarkTheme();
const theme = EditorView.theme({
'&': {
height: '100%',
padding: '.375rem .75rem',
},
'&.cm-focused': {
outline: 'none',
},
'.cm-content': {
width: '100%',
},
'.cm-line': {
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
},
'.ͼ7, .ͼ6': {
textDecoration: 'none',
},
'.cm-cursor': {
'border-left-color': isDark ? 'white' : 'black',
},
});
const startState = EditorState.create({
extensions: [
minimalSetup,
markdown({
codeLanguages: languages,
base: markdownLanguage,
}),
theme,
placeholder(placeholderText),
EditorView.lineWrapping,
editableCompartment.of(EditorView.editable.of(true)),
],
});
const view = new EditorView({
parent: editorRef.current,
state: startState,
});
const cm = createEditorUtils(view as Editor);
cm.setReadOnly = (readOnly: boolean) => {
cm.dispatch({
effects: editableCompartment.reconfigure(
EditorView.editable.of(!readOnly),
),
});
};
if (autoFocus) {
setTimeout(() => {
cm.focus();
}, 10);
}
cm.on('change', () => {
const newValue = cm.getValue();
setValue(newValue);
});
cm.on('focus', () => {
onFocus?.();
});
cm.on('blur', () => {
onBlur?.();
});
setEditor(cm);
return cm;
};
useEffect(() => {
onChange?.(value);
}, [value]);
useEffect(() => {
if (!editorRef.current) {
return;
}
if (editorRef.current.children.length > 0 || editor) {
return;
}
init();
}, [editor]);
return editor;
};