feat: improve copy paste

This commit is contained in:
fantasticit 2022-03-22 20:31:21 +08:00
parent 187cdaf17c
commit 9ddab5a134
50 changed files with 473 additions and 227 deletions

View File

@ -1,5 +1,5 @@
.wrap { .wrap {
margin: 8px 0; margin: 8px 0;
display: flex; display: inline-flex;
justify-content: center; justify-content: center;
} }

View File

@ -1,5 +1,5 @@
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { Popover, TextArea, Typography, Space } from '@douyinfe/semi-ui'; import { Popover, TextArea, Typography, Space } from '@douyinfe/semi-ui';
import { IconHelpCircle } from '@douyinfe/semi-icons'; import { IconHelpCircle } from '@douyinfe/semi-icons';
import katex from 'katex'; import katex from 'katex';
@ -10,6 +10,9 @@ const { Text } = Typography;
export const KatexWrapper = ({ editor, node, updateAttributes }) => { export const KatexWrapper = ({ editor, node, updateAttributes }) => {
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
const { text } = node.attrs; const { text } = node.attrs;
console.log(node.attrs);
const formatText = useMemo(() => { const formatText = useMemo(() => {
try { try {
return katex.renderToString(`${text}`); return katex.renderToString(`${text}`);
@ -24,8 +27,12 @@ export const KatexWrapper = ({ editor, node, updateAttributes }) => {
<span contentEditable={false}></span> <span contentEditable={false}></span>
); );
// useEffect(() => {
// updateAttributes(node.attrs);
// }, []);
return ( return (
<NodeViewWrapper as="div" className={styles.wrap} contentEditable={false}> <NodeViewWrapper as="span" className={styles.wrap} contentEditable={false}>
{isEditable ? ( {isEditable ? (
<Popover <Popover
showArrow showArrow

View File

@ -1,7 +1,7 @@
import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core'; import { Node, Command, mergeAttributes, wrappingInputRule } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { BannerWrapper } from '../components/banner'; import { BannerWrapper } from '../components/banner';
import { typesAvailable } from '../services/markdown/markdownBanner'; import { typesAvailable } from '../services/markdown/markdownToHTML/markdownBanner';
declare module '@tiptap/core' { declare module '@tiptap/core' {
interface Commands<ReturnType> { interface Commands<ReturnType> {

View File

@ -19,18 +19,16 @@ export const HorizontalRule = Node.create<HorizontalRuleOptions>({
addOptions() { addOptions() {
return { return {
HTMLAttributes: { HTMLAttributes: {},
class: 'hr-line',
},
}; };
}, },
parseHTML() { parseHTML() {
return [{ tag: 'div[class=hr-line]' }]; return [{ tag: 'hr' }];
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; return ['hr', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
}, },
addCommands() { addCommands() {

View File

@ -14,26 +14,36 @@ export const KatexInputRegex = /^\$\$(.+)?\$\$$/;
export const Katex = Node.create({ export const Katex = Node.create({
name: 'katex', name: 'katex',
group: 'block', group: 'inline',
defining: true, inline: true,
draggable: true,
selectable: true, selectable: true,
atom: true, atom: true,
addOptions() {
return {
HTMLAttributes: {
class: 'katex',
},
};
},
addAttributes() { addAttributes() {
return { return {
text: { text: {
default: '', default: '',
parseHTML: (element) => {
return element.getAttribute('data-text');
},
}, },
}; };
}, },
parseHTML() { parseHTML() {
return [{ tag: 'div[data-type=katex]' }]; return [{ tag: 'span.katex' }];
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes)]; return ['span', mergeAttributes((this.options && this.options.HTMLAttributes) || {}, HTMLAttributes)];
}, },
// @ts-ignore // @ts-ignore

View File

@ -1,6 +1,6 @@
import { Extension } from '@tiptap/core'; import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from 'prosemirror-state'; import { Plugin, PluginKey } from 'prosemirror-state';
import { markdownSerializer } from '../services/markdown'; import { markdownSerializer } from '../services/markdown/serializer';
import { EXTENSION_PRIORITY_HIGHEST } from '../constants'; import { EXTENSION_PRIORITY_HIGHEST } from '../constants';
import { handleFileEvent } from '../services/upload'; import { handleFileEvent } from '../services/upload';
import { isInCode, LANGUAGES } from '../services/code'; import { isInCode, LANGUAGES } from '../services/code';
@ -63,16 +63,14 @@ export const Paste = Extension.create({
// 处理 markdown // 处理 markdown
if (isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') { if (isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
event.preventDefault(); event.preventDefault();
// FIXME: 由于 title schema 的存在导致反序列化必有 title 节点存在
const firstNode = view.props.state.doc.content.firstChild; const firstNode = view.props.state.doc.content.firstChild;
const hasTitle = isTitleNode(firstNode) && firstNode.content.size > 0; const hasTitle = isTitleNode(firstNode) && firstNode.content.size > 0;
let schema = view.props.state.schema; const schema = view.props.state.schema;
const doc = markdownSerializer.deserialize({ const doc = markdownSerializer.markdownToProsemirror({
schema, schema,
content: normalizePastedMarkdown(text), content: normalizePastedMarkdown(text),
hasTitle, hasTitle,
}); });
// @ts-ignore // @ts-ignore
const transaction = view.state.tr.insert(view.state.selection.head, view.state.schema.nodeFromJSON(doc)); const transaction = view.state.tr.insert(view.state.selection.head, view.state.schema.nodeFromJSON(doc));
view.dispatch(transaction); view.dispatch(transaction);
@ -113,7 +111,7 @@ export const Paste = Extension.create({
if (!doc) { if (!doc) {
return ''; return '';
} }
const content = markdownSerializer.serialize({ const content = markdownSerializer.proseMirrorToMarkdown({
schema: this.editor.schema, schema: this.editor.schema,
content: doc, content: doc,
}); });

View File

@ -21,6 +21,8 @@ export const Paragraph = ({ editor }) => {
} }
}, []); }, []);
console.log(getCurrentCaretTitle(editor));
return ( return (
<Select <Select
disabled={isTitleActive(editor)} disabled={isTitleActive(editor)}

View File

@ -0,0 +1 @@
> 将 HTML 转换成 prosemirror node

View File

@ -0,0 +1,7 @@
import { Renderer } from './renderer';
const renderer = new Renderer();
export const htmlToPromsemirror = (body) => {
return renderer.render(body);
};

View File

@ -1,4 +1,4 @@
import { Mark } from './Mark'; import { Mark } from './mark';
export class Bold extends Mark { export class Bold extends Mark {
matching() { matching() {

View File

@ -1,4 +1,5 @@
import { Mark } from './Mark'; import { Mark } from './mark';
export class Code extends Mark { export class Code extends Mark {
matching() { matching() {
if (this.DOMNode.parentNode.nodeName === 'PRE') { if (this.DOMNode.parentNode.nodeName === 'PRE') {

View File

@ -1,4 +1,4 @@
import { Mark } from './Mark'; import { Mark } from './mark';
export class Italic extends Mark { export class Italic extends Mark {
matching() { matching() {
return this.DOMNode.nodeName === 'EM'; return this.DOMNode.nodeName === 'EM';

View File

@ -1,4 +1,4 @@
import { Mark } from './Mark'; import { Mark } from './mark';
export class Link extends Mark { export class Link extends Mark {
matching() { matching() {
return this.DOMNode.nodeName === 'A'; return this.DOMNode.nodeName === 'A';

View File

@ -1,4 +1,7 @@
export class Mark { export class Mark {
type: string;
DOMNode: HTMLElement;
constructor(DomNode) { constructor(DomNode) {
this.type = 'mark'; this.type = 'mark';
this.DOMNode = DomNode; this.DOMNode = DomNode;

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class Blockquote extends Node { export class Blockquote extends Node {
type = 'blockquote'; type = 'blockquote';

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class BulletList extends Node { export class BulletList extends Node {
type = 'bulletList'; type = 'bulletList';
@ -6,10 +6,4 @@ export class BulletList extends Node {
matching() { matching() {
return this.DOMNode.nodeName === 'UL'; return this.DOMNode.nodeName === 'UL';
} }
// data() {
// return {
// type: 'bulletList',
// };
// }
} }

View File

@ -0,0 +1,9 @@
import { Node } from './node';
export class CodeBlock extends Node {
type = 'codeBlock';
matching() {
return this.DOMNode.nodeName === 'CODE' && this.DOMNode.parentNode.nodeName === 'PRE';
}
}

View File

@ -1,4 +1,5 @@
import { Node } from './Node'; import { Node } from './node';
export class CodeBlockWrapper extends Node { export class CodeBlockWrapper extends Node {
matching() { matching() {
return this.DOMNode.nodeName === 'PRE'; return this.DOMNode.nodeName === 'PRE';

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class HardBreak extends Node { export class HardBreak extends Node {
type = 'hardBreak'; type = 'hardBreak';

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class Heading extends Node { export class Heading extends Node {
type = 'heading'; type = 'heading';
@ -11,12 +11,12 @@ export class Heading extends Node {
return Boolean(this.getLevel()); return Boolean(this.getLevel());
} }
// data() { data() {
// return { return {
// type: 'heading', type: 'heading',
// attrs: { attrs: {
// level: this.getLevel(), level: this.getLevel(),
// }, },
// }; };
// } }
} }

View File

@ -0,0 +1,9 @@
import { Node } from './node';
export class HorizontalRule extends Node {
type = 'horizontalRule';
matching() {
return this.DOMNode.nodeName === 'HR';
}
}

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class Image extends Node { export class Image extends Node {
type = 'image'; type = 'image';

View File

@ -0,0 +1,9 @@
import { Node } from './node';
export class Katex extends Node {
type = 'katex';
matching() {
return this.DOMNode.nodeName === 'SPAN' && this.DOMNode.classList.contains('katex');
}
}

View File

@ -0,0 +1,16 @@
import { Node } from './node';
export class ListItem extends Node {
constructor(DomNode) {
super(DomNode);
this.wrapper = {
type: 'paragraph',
};
}
type = 'listItem';
matching() {
return this.DOMNode.nodeName === 'LI';
}
}

View File

@ -1,7 +1,7 @@
import { getAttributes } from '../utils'; import { getAttributes } from '../utils';
export class Node { export class Node {
wrapper: null; wrapper: unknown;
type = 'node'; type = 'node';
DOMNode: HTMLElement; DOMNode: HTMLElement;
@ -14,7 +14,7 @@ export class Node {
return false; return false;
} }
data() { data(): Record<string, unknown> {
return { return {
type: this.type, type: this.type,
attrs: getAttributes(this.type, this.DOMNode), attrs: getAttributes(this.type, this.DOMNode),

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class OrderedList extends Node { export class OrderedList extends Node {
type = 'orderedList'; type = 'orderedList';

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class Paragraph extends Node { export class Paragraph extends Node {
type = 'paragraph'; type = 'paragraph';

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class Table extends Node { export class Table extends Node {
type = 'table'; type = 'table';

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class TableCell extends Node { export class TableCell extends Node {
type = 'tableCell'; type = 'tableCell';

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class TableHeader extends Node { export class TableHeader extends Node {
type = 'tableHeader'; type = 'tableHeader';

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class TableRow extends Node { export class TableRow extends Node {
type = 'tableRow'; type = 'tableRow';

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class TaskList extends Node { export class TaskList extends Node {
type = 'taskList'; type = 'taskList';

View File

@ -1,4 +1,4 @@
import { Node } from './Node'; import { Node } from './node';
export class TaskListItem extends Node { export class TaskListItem extends Node {
type = 'taskItem'; type = 'taskItem';

View File

@ -1,4 +1,5 @@
import { Node } from './Node'; import { Node } from './node';
export class Text extends Node { export class Text extends Node {
matching() { matching() {
return this.DOMNode.nodeName === '#text'; return this.DOMNode.nodeName === '#text';

View File

@ -1,29 +1,42 @@
import { BulletList } from './Nodes/BulletList'; // nodes
import { CodeBlock } from './Nodes/CodeBlock'; import { CodeBlock } from './nodes/codeBlock';
import { CodeBlockWrapper } from './Nodes/CodeBlockWrapper'; import { CodeBlockWrapper } from './nodes/codeBlockWrapper';
import { HardBreak } from './Nodes/HardBreak'; import { HardBreak } from './nodes/hardBreak';
import { Heading } from './Nodes/Heading'; import { Heading } from './nodes/heading';
import { Image } from './Nodes/Image'; import { Image } from './nodes/image';
import { ListItem } from './Nodes/ListItem'; import { HorizontalRule } from './nodes/horizontalRule';
import { OrderedList } from './Nodes/OrderedList'; import { Blockquote } from './nodes/blockQuote';
import { Paragraph } from './Nodes/Paragraph';
import { Text } from './Nodes/Text';
import { Blockquote } from './Nodes/blockQuote';
import { Table } from './Nodes/table'; // 文本
import { TableHeader } from './Nodes/tableHeader'; import { Katex } from './nodes/katex';
import { TableRow } from './Nodes/tableRow'; import { Paragraph } from './nodes/paragraph';
import { TableCell } from './Nodes/tableCell'; import { Text } from './nodes/text';
import { TaskList } from './Nodes/taskList'; // 表格
import { TaskListItem } from './Nodes/taskListItem'; import { Table } from './nodes/table';
import { TableHeader } from './nodes/tableHeader';
import { TableRow } from './nodes/tableRow';
import { TableCell } from './nodes/tableCell';
import { Bold } from './Marks/Bold'; // 列表
import { Code } from './Marks/Code'; import { TaskList } from './nodes/taskList';
import { Italic } from './Marks/Italic'; import { TaskListItem } from './nodes/taskListItem';
import { Link } from './Marks/Link'; import { ListItem } from './nodes/listItem';
import { OrderedList } from './nodes/orderedList';
import { BulletList } from './nodes/bulletList';
// marks
import { Bold } from './marks/bold';
import { Code } from './marks/code';
import { Italic } from './marks/italic';
import { Link } from './marks/link';
export class Renderer { export class Renderer {
document: HTMLElement;
nodes = [];
marks = [];
storedMarks = [];
constructor() { constructor() {
this.document = undefined; this.document = undefined;
this.storedMarks = []; this.storedMarks = [];
@ -34,7 +47,11 @@ export class Renderer {
HardBreak, HardBreak,
Heading, Heading,
Image, Image,
HorizontalRule,
Katex,
Paragraph, Paragraph,
Text, Text,
Blockquote, Blockquote,
@ -59,22 +76,15 @@ export class Renderer {
} }
stripWhitespace(value) { stripWhitespace(value) {
// return minify(value, {
// collapseWhitespace: true,
// });
return value; return value;
} }
getDocumentBody() { getDocumentBody() {
return this.document; return this.document;
// return this.document.window.document.querySelector('body');
} }
render(value) { render(value) {
this.setDocument(value); this.setDocument(value);
console.log(value);
const content = this.renderChildren(this.getDocumentBody()); const content = this.renderChildren(this.getDocumentBody());
return { return {
@ -151,7 +161,6 @@ export class Renderer {
for (let i in classes) { for (let i in classes) {
const Class = classes[i]; const Class = classes[i];
const instance = new Class(node); const instance = new Class(node);
// console.log(node);
if (instance.matching()) { if (instance.matching()) {
return instance; return instance;
} }

View File

@ -0,0 +1,60 @@
import { BaseKit } from '../../../basekit';
/**
* tiptap extension DOM
* @param element
* @param ret
* @param config
* @returns
*/
const getAttribute = (
element: HTMLElement,
ret = {},
config: Record<string, { default: unknown; parseHTML?: (element: HTMLElement) => Record<string, unknown> }>
) => {
return Object.keys(config).reduce((accu, key) => {
const conf = config[key];
accu[key] = conf.default;
if (conf.parseHTML) {
// try {
accu[key] = conf.parseHTML(element);
// } catch (e) {
//
// }
}
return accu;
}, ret);
};
export const getAttributes = (name: string, element: HTMLElement): Record<string, unknown> => {
const ext = BaseKit.find((ext) => ext.name === name);
if (!ext) return {};
let { config } = ext;
let parent = ext && ext.parent;
if (parent) {
while (parent.parent) {
parent = parent.parent;
}
config = parent.config;
}
if (!config) return {};
const { addGlobalAttributes, addAttributes } = config;
const attrs = {};
if (addGlobalAttributes) {
getAttribute(element, attrs, addGlobalAttributes.call(ext));
}
if (addAttributes) {
getAttribute(element, attrs, addAttributes.call(ext));
}
return attrs;
};

View File

@ -1,21 +1,20 @@
import { sanitize } from 'dompurify';
import markdownit from 'markdown-it'; import markdownit from 'markdown-it';
import sub from 'markdown-it-sub'; import sub from 'markdown-it-sub';
import sup from 'markdown-it-sup'; import sup from 'markdown-it-sup';
import footnote from 'markdown-it-footnote';
import anchor from 'markdown-it-anchor'; import anchor from 'markdown-it-anchor';
import emoji from 'markdown-it-emoji'; import emoji from 'markdown-it-emoji';
import katex from '@traptitech/markdown-it-katex'; import katex from './markdownKatex';
import tasklist from './markdownTaskList'; import tasklist from './markdownTaskList';
import splitMixedLists from './markedownSplitMixedList'; import splitMixedLists from './markedownSplitMixedList';
import markdownUnderline from './markdownUnderline'; import markdownUnderline from './markdownUnderline';
import markdownBanner from './markdownBanner'; import markdownBanner from './markdownBanner';
import { markdownItTable } from './markdownTable'; import { markdownItTable } from './markdownTable';
export const markdown = markdownit('commonmark') const markdown = markdownit('commonmark')
.enable('strikethrough') .enable('strikethrough')
.use(sub) .use(sub)
.use(sup) .use(sup)
.use(footnote)
.use(anchor) .use(anchor)
.use(tasklist) .use(tasklist)
.use(splitMixedLists) .use(splitMixedLists)
@ -25,4 +24,6 @@ export const markdown = markdownit('commonmark')
.use(emoji) .use(emoji)
.use(katex); .use(katex);
export * from './serializer'; export const markdownToHTML = (rawMarkdown) => {
return sanitize(markdown.render(rawMarkdown), {});
};

View File

@ -0,0 +1,225 @@
// var katex = require('katex');
// Test if potential opening or closing delimieter
// Assumes that there is a "$" at state.src[pos]
function isValidDelim(state, pos) {
var prevChar,
nextChar,
max = state.posMax,
can_open = true,
can_close = true;
prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1;
nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1;
// Check non-whitespace conditions for opening and closing, and
// check that closing delimeter isn't followed by a number
if (
prevChar === 0x20 /* " " */ ||
prevChar === 0x09 /* \t */ ||
(nextChar >= 0x30 /* "0" */ && nextChar <= 0x39) /* "9" */
) {
can_close = false;
}
if (nextChar === 0x20 /* " " */ || nextChar === 0x09 /* \t */) {
can_open = false;
}
return {
can_open: can_open,
can_close: can_close,
};
}
function math_inline(state, silent) {
var start, match, token, res, pos, esc_count;
if (state.src[state.pos] !== '$') {
return false;
}
res = isValidDelim(state, state.pos);
if (!res.can_open) {
if (!silent) {
state.pending += '$';
}
state.pos += 1;
return true;
}
// First check for and bypass all properly escaped delimieters
// This loop will assume that the first leading backtick can not
// be the first character in state.src, which is known since
// we have found an opening delimieter already.
start = state.pos + 1;
match = start;
while ((match = state.src.indexOf('$', match)) !== -1) {
// Found potential $, look for escapes, pos will point to
// first non escape when complete
pos = match - 1;
while (state.src[pos] === '\\') {
pos -= 1;
}
// Even number of escapes, potential closing delimiter found
if ((match - pos) % 2 == 1) {
break;
}
match += 1;
}
// No closing delimter found. Consume $ and continue.
if (match === -1) {
if (!silent) {
state.pending += '$';
}
state.pos = start;
return true;
}
// Check if we have empty content, ie: $$. Do not parse.
if (match - start === 0) {
if (!silent) {
state.pending += '$$';
}
state.pos = start + 1;
return true;
}
// Check for valid closing delimiter
res = isValidDelim(state, match);
if (!res.can_close) {
if (!silent) {
state.pending += '$';
}
state.pos = start;
return true;
}
if (!silent) {
token = state.push('math_inline', 'math', 0);
token.markup = '$';
token.content = state.src.slice(start, match);
}
state.pos = match + 1;
return true;
}
function math_block(state, start, end, silent) {
var firstLine,
lastLine,
next,
lastPos,
found = false,
token,
pos = state.bMarks[start] + state.tShift[start],
max = state.eMarks[start];
if (pos + 2 > max) {
return false;
}
if (state.src.slice(pos, pos + 2) !== '$$') {
return false;
}
pos += 2;
firstLine = state.src.slice(pos, max);
if (silent) {
return true;
}
if (firstLine.trim().slice(-2) === '$$') {
// Single line expression
firstLine = firstLine.trim().slice(0, -2);
found = true;
}
for (next = start; !found; ) {
next++;
if (next >= end) {
break;
}
pos = state.bMarks[next] + state.tShift[next];
max = state.eMarks[next];
if (pos < max && state.tShift[next] < state.blkIndent) {
// non-empty line with negative indent should stop the list:
break;
}
if (state.src.slice(pos, max).trim().slice(-2) === '$$') {
lastPos = state.src.slice(0, max).lastIndexOf('$$');
lastLine = state.src.slice(pos, lastPos);
found = true;
}
}
state.line = next + 1;
token = state.push('math_block', 'math', 0);
token.block = true;
token.content =
(firstLine && firstLine.trim() ? firstLine + '\n' : '') +
state.getLines(start + 1, next, state.tShift[start], true) +
(lastLine && lastLine.trim() ? lastLine : '');
token.map = [start, state.line];
token.markup = '$$';
return true;
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
var katex = {
renderToString: (s, opts) => s,
};
export default function math_plugin(md, options) {
// Default options
options = options || {};
if (options.katex) {
katex = options.katex;
}
if (!options.blockClass) {
options.blockClass = '';
}
var inlineRenderer = function (tokens, idx) {
return katexBlock(tokens[idx].content);
};
var katexBlock = function (latex) {
options.displayMode = true;
try {
return `<span class="katex ${options.blockClass}" data-text="${katex.renderToString(latex, options)}"></span>`;
} catch (error) {
if (options.throwOnError) {
console.log(error);
}
return `<span class='katex katex-error ${options.blockClass}' data-error='${escapeHtml(
error.toString()
)}' data-text="${escapeHtml(latex)}"></span>`;
}
};
var blockRenderer = function (tokens, idx) {
return katexBlock(tokens[idx].content) + '\n';
};
md.inline.ruler.after('escape', 'math_inline', math_inline);
md.block.ruler.after('blockquote', 'math_block', math_block, {
alt: ['paragraph', 'reference', 'blockquote', 'list'],
});
md.renderer.rules.math_inline = inlineRenderer;
md.renderer.rules.math_block = blockRenderer;
}

View File

@ -7,7 +7,7 @@ export default function splitMixedLists(md) {
for (let i = 0; i < tokens.length; i++) { for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]; const token = tokens[i];
if (token.attrGet('class') !== 'contains-task-list') { if (token.attrGet('class') !== 'task-list') {
continue; continue;
} }
const firstChild = tokens[i + 1]; const firstChild = tokens[i + 1];

View File

@ -1,7 +1,4 @@
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
import { sanitize } from 'dompurify';
import { MarkdownSerializer as ProseMirrorMarkdownSerializer, defaultMarkdownSerializer } from 'prosemirror-markdown'; import { MarkdownSerializer as ProseMirrorMarkdownSerializer, defaultMarkdownSerializer } from 'prosemirror-markdown';
import { markdown } from '.';
import { Attachment } from '../../extensions/attachment'; import { Attachment } from '../../extensions/attachment';
import { Banner } from '../../extensions/banner'; import { Banner } from '../../extensions/banner';
import { Blockquote } from '../../extensions/blockquote'; import { Blockquote } from '../../extensions/blockquote';
@ -48,12 +45,8 @@ import {
renderImage, renderImage,
renderHTMLNode, renderHTMLNode,
} from './serializerHelpers'; } from './serializerHelpers';
import { htmlToPromsemirror } from './htmlToProsemirror';
// import * as HTML/ from 'html-to-prosemirror' import { markdownToHTML } from './markdownToHTML';
import { Renderer } from './src/Renderer';
const renderer = new Renderer();
const defaultSerializerConfig = { const defaultSerializerConfig = {
marks: { marks: {
@ -188,48 +181,41 @@ const defaultSerializerConfig = {
}, },
}; };
const renderMarkdown = (rawMarkdown) => {
return sanitize(markdown.render(rawMarkdown), {});
};
const createMarkdownSerializer = () => ({ const createMarkdownSerializer = () => ({
// 将 markdown 字符串转换为 ProseMirror JSONDocument // 将 markdown 字符串转换为 ProseMirror JSONDocument
deserialize: ({ schema, content, hasTitle }) => { markdownToProsemirror: ({ schema, content, hasTitle }) => {
const html = renderMarkdown(content); const html = markdownToHTML(content);
if (!html) return null; if (!html) return null;
const parser = new DOMParser(); const parser = new DOMParser();
const { body } = parser.parseFromString(html, 'text/html'); const { body } = parser.parseFromString(html, 'text/html');
body.append(document.createComment(content)); body.append(document.createComment(content));
const json = renderer.render(body); const json = htmlToPromsemirror(body);
console.log({ hasTitle, json, body }); console.log({ hasTitle, json, body });
// 设置标题
if (!hasTitle) { if (!hasTitle) {
const firstNode = json.content[0]; const firstNode = json.content[0];
if (firstNode) { if (firstNode) {
if (firstNode.type === 'heading') { if (firstNode.type === 'heading' || firstNode.type === 'paragraph') {
firstNode.type = 'title'; firstNode.type = 'title';
} }
} }
} }
const nodes = json.content; const nodes = json.content;
const result = { type: 'doc', content: [] }; const result = { type: 'doc', content: [] };
for (let i = 0; i < nodes.length; ) { for (let i = 0; i < nodes.length; ) {
const node = nodes[i]; const node = nodes[i];
// 目的:合并成 promirror 需要的 table 格式
if (node.type === 'tableRow') { if (node.type === 'tableRow') {
const nextNode = nodes[i + 1]; const nextNode = nodes[i + 1];
if (nextNode && nextNode.type === 'table') { if (nextNode && nextNode.type === 'table') {
nextNode.content.unshift(node); nextNode.content.unshift(node);
result.content.push(nextNode); result.content.push(nextNode);
i += 2; i += 2;
} else {
// 出错了!!
} }
} else { } else {
result.content.push(node); result.content.push(node);
@ -238,13 +224,10 @@ const createMarkdownSerializer = () => ({
} }
return result; return result;
// const state = ProseMirrorDOMParser.fromSchema(schema).parse(body);
// return state.toJSON();
}, },
// 将 ProseMirror JSONDocument 转换为 markdown 字符串 // 将 ProseMirror JSONDocument 转换为 markdown 字符串
serialize: ({ schema, content }) => { proseMirrorToMarkdown: ({ schema, content }) => {
const serializer = new ProseMirrorMarkdownSerializer( const serializer = new ProseMirrorMarkdownSerializer(
{ {
...defaultSerializerConfig.nodes, ...defaultSerializerConfig.nodes,

View File

@ -1,29 +0,0 @@
import { Node } from './Node';
export class CodeBlock extends Node {
type = 'codeBlock';
matching() {
return this.DOMNode.nodeName === 'CODE' && this.DOMNode.parentNode.nodeName === 'PRE';
}
// getLanguage() {
// const language = this.DOMNode.getAttribute('class');
// return language ? language.replace(/^language-/, '') : language;
// }
// data() {
// const language = this.getLanguage();
// if (language) {
// return {
// type: 'codeBlock',
// attrs: {
// language,
// },
// };
// }
// return {
// type: 'codeBlock',
// };
// }
}

View File

@ -1,21 +0,0 @@
import { Node } from './Node';
export class ListItem extends Node {
constructor(...args) {
super(...args);
this.wrapper = {
type: 'paragraph',
};
}
type = 'listItem';
matching() {
return this.DOMNode.nodeName === 'LI';
}
// data() {
// if (this.DOMNode.childNodes.length === 1 && this.DOMNode.childNodes[0].nodeName === 'P') {
// this.wrapper = null;
// }
// }
}

View File

@ -1,47 +0,0 @@
import { BaseKit } from '../../../basekit';
export const getAttributes = (name: string, element: HTMLElement): Record<string, unknown> => {
const ext = BaseKit.find((ext) => ext.name === name);
const run = (
ret = {},
config: Record<string, { default: unknown; parseHTML?: (element: HTMLElement) => Record<string, unknown> }>
) => {
return Object.keys(config).reduce((accu, key) => {
const conf = config[key];
accu[key] = conf.default;
if (conf.parseHTML) {
try {
accu[key] = conf.parseHTML(element);
} catch (e) {
//
}
}
return accu;
}, ret);
};
let parent = ext && ext.parent;
if (!parent) return {};
while (parent.parent) {
parent = parent.parent;
}
const { config } = parent;
const { addGlobalAttributes, addAttributes } = config;
const attrs = {};
if (addGlobalAttributes) {
run(attrs, addGlobalAttributes.call(ext));
}
if (addAttributes) {
run(attrs, addAttributes.call(ext));
}
return attrs;
};

View File

@ -14,16 +14,15 @@ export const useTheme = () => {
}; };
useEffect(() => { useEffect(() => {
const body = document.body; // const body = document.body;
if (theme === 'dark') { // if (theme === 'dark') {
body.setAttribute('theme-mode', 'dark'); // body.setAttribute('theme-mode', 'dark');
return; // return;
} // }
// if (theme === 'light') {
if (theme === 'light') { // body.setAttribute('theme-mode', 'light');
body.setAttribute('theme-mode', 'light'); // return;
return; // }
}
}, [theme]); }, [theme]);
useEffect(() => { useEffect(() => {

View File

@ -54,8 +54,8 @@
} }
} }
.hr-line { hr {
width: 100%; border: 0;
height: 2px; height: 2px;
background: var(--semi-color-border); background: var(--semi-color-border);
margin: 18px 0; margin: 18px 0;

View File

@ -25,7 +25,7 @@ export class DocumentEntity {
@Column({ type: 'text', comment: '文档内容' }) @Column({ type: 'text', comment: '文档内容' })
public content: string; public content: string;
@Column({ type: 'blob', comment: '文档内容' }) @Column({ type: 'longblob', comment: '文档内容' })
public state: Uint8Array; public state: Uint8Array;
@Column({ @Column({