mirror of https://github.com/fantasticit/think.git
feat: html -> prosemirror node
This commit is contained in:
parent
67a5ae54cf
commit
187cdaf17c
|
@ -5,6 +5,7 @@ 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';
|
||||||
import { isMarkdown, normalizePastedMarkdown } from '../services/markdown/helpers';
|
import { isMarkdown, normalizePastedMarkdown } from '../services/markdown/helpers';
|
||||||
|
import { isTitleNode } from '../services/node';
|
||||||
|
|
||||||
export const Paste = Extension.create({
|
export const Paste = Extension.create({
|
||||||
name: 'paste',
|
name: 'paste',
|
||||||
|
@ -63,14 +64,17 @@ export const Paste = Extension.create({
|
||||||
if (isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
|
if (isMarkdown(text) || html.length === 0 || pasteCodeLanguage === 'markdown') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// FIXME: 由于 title schema 的存在导致反序列化必有 title 节点存在
|
// FIXME: 由于 title schema 的存在导致反序列化必有 title 节点存在
|
||||||
// const hasTitle = isTitleNode(view.props.state.doc.content.firstChild);
|
const firstNode = view.props.state.doc.content.firstChild;
|
||||||
|
const hasTitle = isTitleNode(firstNode) && firstNode.content.size > 0;
|
||||||
let schema = view.props.state.schema;
|
let schema = view.props.state.schema;
|
||||||
const doc = markdownSerializer.deserialize({
|
const doc = markdownSerializer.deserialize({
|
||||||
schema,
|
schema,
|
||||||
content: normalizePastedMarkdown(text),
|
content: normalizePastedMarkdown(text),
|
||||||
|
hasTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const transaction = view.state.tr.insert(view.state.selection.head, doc);
|
const transaction = view.state.tr.insert(view.state.selection.head, view.state.schema.nodeFromJSON(doc));
|
||||||
view.dispatch(transaction);
|
view.dispatch(transaction);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,10 @@
|
||||||
import { wrappingInputRule } from '@tiptap/core';
|
import { wrappingInputRule, mergeAttributes } from '@tiptap/core';
|
||||||
import { TaskItem as BuiltInTaskItem } from '@tiptap/extension-task-item';
|
import { TaskItem as BuiltInTaskItem } from '@tiptap/extension-task-item';
|
||||||
import { Plugin } from 'prosemirror-state';
|
import { Plugin } from 'prosemirror-state';
|
||||||
import { findParentNodeClosestToPos } from 'prosemirror-utils';
|
import { findParentNodeClosestToPos } from 'prosemirror-utils';
|
||||||
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||||
|
|
||||||
const CustomTaskItem = BuiltInTaskItem.extend({
|
const CustomTaskItem = BuiltInTaskItem.extend({
|
||||||
addOptions() {
|
|
||||||
return {
|
|
||||||
nested: true,
|
|
||||||
HTMLAttributes: {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
checked: {
|
|
||||||
default: false,
|
|
||||||
parseHTML: (element) => {
|
|
||||||
const checkbox = element.querySelector('input[type=checkbox].task-list-item-checkbox');
|
|
||||||
// @ts-ignore
|
|
||||||
return checkbox?.checked;
|
|
||||||
},
|
|
||||||
renderHTML: (attributes) => ({
|
|
||||||
'data-checked': attributes.checked,
|
|
||||||
}),
|
|
||||||
keepOnSplit: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -51,35 +27,35 @@ const CustomTaskItem = BuiltInTaskItem.extend({
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
// addProseMirrorPlugins() {
|
||||||
return [
|
// return [
|
||||||
new Plugin({
|
// new Plugin({
|
||||||
props: {
|
// props: {
|
||||||
// @ts-ignore
|
// // @ts-ignore
|
||||||
handleClick: (view, pos, event) => {
|
// handleClick: (view, pos, event) => {
|
||||||
const state = view.state;
|
// const state = view.state;
|
||||||
const schema = state.schema;
|
// const schema = state.schema;
|
||||||
|
|
||||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
// const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||||
const position = state.doc.resolve(coordinates.pos);
|
// const position = state.doc.resolve(coordinates.pos);
|
||||||
const parentList = findParentNodeClosestToPos(position, function (node) {
|
// const parentList = findParentNodeClosestToPos(position, function (node) {
|
||||||
return node.type === schema.nodes.taskItem || node.type === schema.nodes.listItem;
|
// return node.type === schema.nodes.taskItem || node.type === schema.nodes.listItem;
|
||||||
});
|
// });
|
||||||
// @ts-ignore
|
// // @ts-ignore
|
||||||
const isListClicked = event.target.tagName.toLowerCase() === 'li';
|
// const isListClicked = event.target.tagName.toLowerCase() === 'li';
|
||||||
if (!isListClicked || !parentList || parentList.node.type !== schema.nodes.taskItem) {
|
// if (!isListClicked || !parentList || parentList.node.type !== schema.nodes.taskItem) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
const tr = state.tr;
|
// const tr = state.tr;
|
||||||
tr.setNodeMarkup(parentList.pos, schema.nodes.taskItem, {
|
// tr.setNodeMarkup(parentList.pos, schema.nodes.taskItem, {
|
||||||
checked: !parentList.node.attrs.checked,
|
// checked: !parentList.node.attrs.checked,
|
||||||
});
|
// });
|
||||||
view.dispatch(tr);
|
// view.dispatch(tr);
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
}),
|
// }),
|
||||||
];
|
// ];
|
||||||
},
|
// },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TaskItem = CustomTaskItem.configure({ nested: true });
|
export const TaskItem = CustomTaskItem.configure({ nested: true });
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { mergeAttributes } from '@tiptap/core';
|
|
||||||
import { TaskList as BuiltInTaskList } from '@tiptap/extension-task-list';
|
import { TaskList as BuiltInTaskList } from '@tiptap/extension-task-list';
|
||||||
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
|
||||||
|
|
||||||
|
@ -11,8 +10,4 @@ export const TaskList = BuiltInTaskList.extend({
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes: { numeric, ...HTMLAttributes } }) {
|
|
||||||
return [numeric ? 'ol' : 'ul', mergeAttributes(HTMLAttributes, { 'data-type': 'taskList' }), 0];
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,27 +3,25 @@ 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 footnote from 'markdown-it-footnote';
|
||||||
import anchor from 'markdown-it-anchor';
|
import anchor from 'markdown-it-anchor';
|
||||||
import tasklist from 'markdown-it-task-lists';
|
|
||||||
import emoji from 'markdown-it-emoji';
|
import emoji from 'markdown-it-emoji';
|
||||||
import katex from '@traptitech/markdown-it-katex';
|
import katex from '@traptitech/markdown-it-katex';
|
||||||
|
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';
|
||||||
|
|
||||||
export const markdown = markdownit({
|
export const markdown = markdownit('commonmark')
|
||||||
html: true,
|
|
||||||
linkify: true,
|
|
||||||
typographer: true,
|
|
||||||
})
|
|
||||||
.enable('strikethrough')
|
.enable('strikethrough')
|
||||||
.use(sub)
|
.use(sub)
|
||||||
.use(sup)
|
.use(sup)
|
||||||
.use(footnote)
|
.use(footnote)
|
||||||
.use(anchor)
|
.use(anchor)
|
||||||
.use(tasklist, { enable: true })
|
.use(tasklist)
|
||||||
.use(splitMixedLists)
|
.use(splitMixedLists)
|
||||||
.use(markdownUnderline)
|
.use(markdownUnderline)
|
||||||
.use(markdownBanner)
|
.use(markdownBanner)
|
||||||
|
.use(markdownItTable)
|
||||||
.use(emoji)
|
.use(emoji)
|
||||||
.use(katex);
|
.use(katex);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,283 @@
|
||||||
|
// Copied from https://github.com/markdown-it/markdown-it/blob/master/lib/rules_block/table.js
|
||||||
|
|
||||||
|
function isSpace(code) {
|
||||||
|
switch (code) {
|
||||||
|
case 0x09:
|
||||||
|
case 0x20:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLine(state, line) {
|
||||||
|
var pos = state.bMarks[line] + state.tShift[line],
|
||||||
|
max = state.eMarks[line];
|
||||||
|
|
||||||
|
return state.src.substr(pos, max - pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapedSplit(str) {
|
||||||
|
var result = [],
|
||||||
|
pos = 0,
|
||||||
|
max = str.length,
|
||||||
|
ch,
|
||||||
|
isEscaped = false,
|
||||||
|
lastPos = 0,
|
||||||
|
current = '';
|
||||||
|
|
||||||
|
ch = str.charCodeAt(pos);
|
||||||
|
|
||||||
|
while (pos < max) {
|
||||||
|
if (ch === 0x7c /* | */) {
|
||||||
|
if (!isEscaped) {
|
||||||
|
// pipe separating cells, '|'
|
||||||
|
result.push(current + str.substring(lastPos, pos));
|
||||||
|
current = '';
|
||||||
|
lastPos = pos + 1;
|
||||||
|
} else {
|
||||||
|
// escaped pipe, '\|'
|
||||||
|
current += str.substring(lastPos, pos - 1);
|
||||||
|
lastPos = pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isEscaped = ch === 0x5c /* \ */;
|
||||||
|
pos++;
|
||||||
|
|
||||||
|
ch = str.charCodeAt(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(current + str.substring(lastPos));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function table(state, startLine, endLine, silent) {
|
||||||
|
var ch,
|
||||||
|
lineText,
|
||||||
|
pos,
|
||||||
|
i,
|
||||||
|
l,
|
||||||
|
nextLine,
|
||||||
|
columns,
|
||||||
|
columnCount,
|
||||||
|
token,
|
||||||
|
aligns,
|
||||||
|
t,
|
||||||
|
tableLines,
|
||||||
|
tbodyLines,
|
||||||
|
oldParentType,
|
||||||
|
terminate,
|
||||||
|
terminatorRules,
|
||||||
|
firstCh,
|
||||||
|
secondCh;
|
||||||
|
|
||||||
|
// should have at least two lines
|
||||||
|
if (startLine + 2 > endLine) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextLine = startLine + 1;
|
||||||
|
|
||||||
|
if (state.sCount[nextLine] < state.blkIndent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's indented more than 3 spaces, it should be a code block
|
||||||
|
if (state.sCount[nextLine] - state.blkIndent >= 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// first character of the second line should be '|', '-', ':',
|
||||||
|
// and no other characters are allowed but spaces;
|
||||||
|
// basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp
|
||||||
|
|
||||||
|
pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
||||||
|
if (pos >= state.eMarks[nextLine]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstCh = state.src.charCodeAt(pos++);
|
||||||
|
if (firstCh !== 0x7c /* | */ && firstCh !== 0x2d /* - */ && firstCh !== 0x3a /* : */) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos >= state.eMarks[nextLine]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
secondCh = state.src.charCodeAt(pos++);
|
||||||
|
if (secondCh !== 0x7c /* | */ && secondCh !== 0x2d /* - */ && secondCh !== 0x3a /* : */ && !isSpace(secondCh)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if first character is '-', then second character must not be a space
|
||||||
|
// (due to parsing ambiguity with list)
|
||||||
|
if (firstCh === 0x2d /* - */ && isSpace(secondCh)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (pos < state.eMarks[nextLine]) {
|
||||||
|
ch = state.src.charCodeAt(pos);
|
||||||
|
|
||||||
|
if (ch !== 0x7c /* | */ && ch !== 0x2d /* - */ && ch !== 0x3a /* : */ && !isSpace(ch)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineText = getLine(state, startLine + 1);
|
||||||
|
|
||||||
|
columns = lineText.split('|');
|
||||||
|
aligns = [];
|
||||||
|
for (i = 0; i < columns.length; i++) {
|
||||||
|
t = columns[i].trim();
|
||||||
|
if (!t) {
|
||||||
|
// allow empty columns before and after table, but not in between columns;
|
||||||
|
// e.g. allow ` |---| `, disallow ` ---||--- `
|
||||||
|
if (i === 0 || i === columns.length - 1) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^:?-+:?$/.test(t)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (t.charCodeAt(t.length - 1) === 0x3a /* : */) {
|
||||||
|
aligns.push(t.charCodeAt(0) === 0x3a /* : */ ? 'center' : 'right');
|
||||||
|
} else if (t.charCodeAt(0) === 0x3a /* : */) {
|
||||||
|
aligns.push('left');
|
||||||
|
} else {
|
||||||
|
aligns.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lineText = getLine(state, startLine).trim();
|
||||||
|
if (lineText.indexOf('|') === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (state.sCount[startLine] - state.blkIndent >= 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
columns = escapedSplit(lineText);
|
||||||
|
if (columns.length && columns[0] === '') columns.shift();
|
||||||
|
if (columns.length && columns[columns.length - 1] === '') columns.pop();
|
||||||
|
|
||||||
|
// header row will define an amount of columns in the entire table,
|
||||||
|
// and align row should be exactly the same (the rest of the rows can differ)
|
||||||
|
columnCount = columns.length;
|
||||||
|
if (columnCount === 0 || columnCount !== aligns.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
oldParentType = state.parentType;
|
||||||
|
state.parentType = 'table';
|
||||||
|
|
||||||
|
// use 'blockquote' lists for termination because it's
|
||||||
|
// the most similar to tables
|
||||||
|
terminatorRules = state.md.block.ruler.getRules('blockquote');
|
||||||
|
|
||||||
|
token = state.push('table_open', 'table', 1);
|
||||||
|
token.map = tableLines = [startLine, 0];
|
||||||
|
|
||||||
|
token = state.push('thead_open', 'thead', 1);
|
||||||
|
token.map = [startLine, startLine + 1];
|
||||||
|
|
||||||
|
token = state.push('tr_open', 'tr', 1);
|
||||||
|
token.map = [startLine, startLine + 1];
|
||||||
|
|
||||||
|
for (i = 0; i < columns.length; i++) {
|
||||||
|
token = state.push('th_open', 'th', 1);
|
||||||
|
if (aligns[i]) {
|
||||||
|
token.attrs = [['style', 'text-align:' + aligns[i]]];
|
||||||
|
}
|
||||||
|
|
||||||
|
token = state.push('paragraph_open', 'p', 1);
|
||||||
|
token = state.push('inline', '', 0);
|
||||||
|
token.content = columns[i].trim();
|
||||||
|
token.children = [];
|
||||||
|
token = state.push('paragraph_close', 'p', -1);
|
||||||
|
|
||||||
|
token = state.push('th_close', 'th', -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
token = state.push('tr_close', 'tr', -1);
|
||||||
|
token = state.push('thead_close', 'thead', -1);
|
||||||
|
|
||||||
|
for (nextLine = startLine + 2; nextLine < endLine; nextLine++) {
|
||||||
|
if (state.sCount[nextLine] < state.blkIndent) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate = false;
|
||||||
|
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
||||||
|
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
||||||
|
terminate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (terminate) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lineText = getLine(state, nextLine).trim();
|
||||||
|
if (!lineText) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (state.sCount[nextLine] - state.blkIndent >= 4) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
columns = escapedSplit(lineText);
|
||||||
|
if (columns.length && columns[0] === '') columns.shift();
|
||||||
|
if (columns.length && columns[columns.length - 1] === '') columns.pop();
|
||||||
|
|
||||||
|
if (nextLine === startLine + 2) {
|
||||||
|
token = state.push('tbody_open', 'tbody', 1);
|
||||||
|
token.map = tbodyLines = [startLine + 2, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
token = state.push('tr_open', 'tr', 1);
|
||||||
|
token.map = [nextLine, nextLine + 1];
|
||||||
|
|
||||||
|
for (i = 0; i < columnCount; i++) {
|
||||||
|
token = state.push('td_open', 'td', 1);
|
||||||
|
if (aligns[i]) {
|
||||||
|
token.attrs = [['style', 'text-align:' + aligns[i]]];
|
||||||
|
}
|
||||||
|
|
||||||
|
token = state.push('paragraph_open', 'p', 1);
|
||||||
|
token = state.push('inline', '', 0);
|
||||||
|
token.content = columns[i].trim();
|
||||||
|
token.children = [];
|
||||||
|
token = state.push('paragraph_close', 'p', -1);
|
||||||
|
|
||||||
|
token = state.push('td_close', 'td', -1);
|
||||||
|
}
|
||||||
|
token = state.push('tr_close', 'tr', -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tbodyLines) {
|
||||||
|
token = state.push('tbody_close', 'tbody', -1);
|
||||||
|
tbodyLines[1] = nextLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
token = state.push('table_close', 'table', -1);
|
||||||
|
tableLines[1] = nextLine;
|
||||||
|
|
||||||
|
state.parentType = oldParentType;
|
||||||
|
state.line = nextLine;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const markdownItTable = (md, options) => {
|
||||||
|
md.block.ruler.before('paragraph', 'table', table, {
|
||||||
|
alt: ['paragraph', 'reference'],
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: ISC
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Markdown-it plugin to render GitHub-style task lists; see
|
||||||
|
//
|
||||||
|
// https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments
|
||||||
|
// https://github.com/blog/1825-task-lists-in-all-markdown-documents
|
||||||
|
|
||||||
|
import MarkdownIt from 'markdown-it/lib';
|
||||||
|
import StateCore from 'markdown-it/lib/rules_core/state_core';
|
||||||
|
import Token from 'markdown-it/lib/token';
|
||||||
|
|
||||||
|
interface TaskListsOptions {
|
||||||
|
enabled: boolean;
|
||||||
|
label: boolean;
|
||||||
|
lineNumber: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkboxRegex = /^ *\[([ x])\] /i;
|
||||||
|
|
||||||
|
export default function markdownItTaskLists(
|
||||||
|
md: MarkdownIt,
|
||||||
|
options: TaskListsOptions = { enabled: true, label: true, lineNumber: false }
|
||||||
|
): void {
|
||||||
|
md.core.ruler.after('inline', 'github-task-lists', (state) => processToken(state, options));
|
||||||
|
md.renderer.rules.taskListItemCheckbox = (tokens) => {
|
||||||
|
const token = tokens[0];
|
||||||
|
const checkedAttribute = token.attrGet('checked') ? 'checked=""' : '';
|
||||||
|
const disabledAttribute = token.attrGet('disabled') ? 'disabled=""' : '';
|
||||||
|
const id = token.attrGet('id');
|
||||||
|
const line = token.attrGet('line');
|
||||||
|
const idAttribute = `id="${id}"`;
|
||||||
|
const dataLineAttribute = line && options.lineNumber ? `data-line="${line}"` : '';
|
||||||
|
|
||||||
|
return `<label contenteditable="false"><input class="task-list-item-checkbox" type="checkbox" ${checkedAttribute} ${disabledAttribute} ${dataLineAttribute} ${idAttribute}"><span></span></label>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
md.renderer.rules.taskListItemLabel_close = () => {
|
||||||
|
return '</p></div>';
|
||||||
|
};
|
||||||
|
|
||||||
|
md.renderer.rules.taskListItemLabel_open = (tokens) => {
|
||||||
|
const token = tokens[0];
|
||||||
|
const id = token.attrGet('id');
|
||||||
|
return `<div><p>`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function attrSet(token, name, value) {
|
||||||
|
var index = token.attrIndex(name);
|
||||||
|
var attr = [name, value];
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
token.attrPush(attr);
|
||||||
|
} else {
|
||||||
|
token.attrs[index] = attr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processToken(state: StateCore, options: TaskListsOptions): boolean {
|
||||||
|
const allTokens = state.tokens;
|
||||||
|
|
||||||
|
attrSet(allTokens[0], 'class', 'contains-task-list');
|
||||||
|
|
||||||
|
for (let i = 2; i < allTokens.length; i++) {
|
||||||
|
if (!isTodoItem(allTokens, i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isChecked } = todoify(allTokens[i], options);
|
||||||
|
allTokens[i - 2].attrJoin('class', `task-list-item`);
|
||||||
|
allTokens[i - 2].attrJoin('data-checked', isChecked ? `true` : `false`);
|
||||||
|
|
||||||
|
const parentToken = findParentToken(allTokens, i - 2);
|
||||||
|
if (parentToken) {
|
||||||
|
parentToken.attrJoin('class', 'task-list');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findParentToken(tokens: Token[], index: number): Token | undefined {
|
||||||
|
const targetLevel = tokens[index].level - 1;
|
||||||
|
for (let currentTokenIndex = index - 1; currentTokenIndex >= 0; currentTokenIndex--) {
|
||||||
|
if (tokens[currentTokenIndex].level === targetLevel) {
|
||||||
|
return tokens[currentTokenIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTodoItem(tokens: Token[], index: number): boolean {
|
||||||
|
return (
|
||||||
|
isInline(tokens[index]) &&
|
||||||
|
isParagraph(tokens[index - 1]) &&
|
||||||
|
isListItem(tokens[index - 2]) &&
|
||||||
|
startsWithTodoMarkdown(tokens[index])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function todoify(token: Token, options: TaskListsOptions) {
|
||||||
|
if (token.children == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = generateIdForToken(token);
|
||||||
|
|
||||||
|
const { checkbox, isChecked } = createCheckboxToken(token, options.enabled, id);
|
||||||
|
token.children.splice(0, 0, checkbox);
|
||||||
|
token.children[1].content = token.children[1].content.replace(checkboxRegex, '');
|
||||||
|
|
||||||
|
if (options.label) {
|
||||||
|
token.children.splice(1, 0, createLabelBeginToken(id));
|
||||||
|
token.children.push(createLabelEndToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isChecked };
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateIdForToken(token: Token): string {
|
||||||
|
if (token.map) {
|
||||||
|
return `task-item-${token.map[0]}`;
|
||||||
|
} else {
|
||||||
|
return `task-item-${Math.ceil(Math.random() * (10000 * 1000) - 1000)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCheckboxToken(token: Token, enabled: boolean, id: string): Token {
|
||||||
|
const checkbox = new Token('taskListItemCheckbox', '', 0);
|
||||||
|
if (!enabled) {
|
||||||
|
checkbox.attrSet('disabled', 'true');
|
||||||
|
}
|
||||||
|
if (token.map) {
|
||||||
|
checkbox.attrSet('line', token.map[0].toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbox.attrSet('id', id);
|
||||||
|
|
||||||
|
const checkboxRegexResult = checkboxRegex.exec(token.content);
|
||||||
|
const isChecked = !!checkboxRegexResult && checkboxRegexResult[1].toLowerCase() === 'x';
|
||||||
|
if (isChecked) {
|
||||||
|
checkbox.attrSet('checked', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { checkbox, isChecked };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLabelBeginToken(id: string): Token {
|
||||||
|
const labelBeginToken = new Token('taskListItemLabel_open', '', 1);
|
||||||
|
labelBeginToken.attrSet('id', id);
|
||||||
|
return labelBeginToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLabelEndToken(): Token {
|
||||||
|
return new Token('taskListItemLabel_close', '', -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInline(token: Token): boolean {
|
||||||
|
return token.type === 'inline';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isParagraph(token: Token): boolean {
|
||||||
|
return token.type === 'paragraph_open';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isListItem(token: Token): boolean {
|
||||||
|
return token.type === 'list_item_open';
|
||||||
|
}
|
||||||
|
|
||||||
|
function startsWithTodoMarkdown(token: Token): boolean {
|
||||||
|
return checkboxRegex.test(token.content);
|
||||||
|
}
|
|
@ -49,6 +49,12 @@ import {
|
||||||
renderHTMLNode,
|
renderHTMLNode,
|
||||||
} from './serializerHelpers';
|
} from './serializerHelpers';
|
||||||
|
|
||||||
|
// import * as HTML/ from 'html-to-prosemirror'
|
||||||
|
|
||||||
|
import { Renderer } from './src/Renderer';
|
||||||
|
|
||||||
|
const renderer = new Renderer();
|
||||||
|
|
||||||
const defaultSerializerConfig = {
|
const defaultSerializerConfig = {
|
||||||
marks: {
|
marks: {
|
||||||
[Bold.name]: defaultMarkdownSerializer.marks.strong,
|
[Bold.name]: defaultMarkdownSerializer.marks.strong,
|
||||||
|
@ -188,14 +194,53 @@ const renderMarkdown = (rawMarkdown) => {
|
||||||
|
|
||||||
const createMarkdownSerializer = () => ({
|
const createMarkdownSerializer = () => ({
|
||||||
// 将 markdown 字符串转换为 ProseMirror JSONDocument
|
// 将 markdown 字符串转换为 ProseMirror JSONDocument
|
||||||
deserialize: ({ schema, content }) => {
|
deserialize: ({ schema, content, hasTitle }) => {
|
||||||
const html = renderMarkdown(content);
|
const html = renderMarkdown(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 state = ProseMirrorDOMParser.fromSchema(schema).parse(body);
|
const json = renderer.render(body);
|
||||||
return state;
|
|
||||||
|
console.log({ hasTitle, json, body });
|
||||||
|
|
||||||
|
if (!hasTitle) {
|
||||||
|
const firstNode = json.content[0];
|
||||||
|
|
||||||
|
if (firstNode) {
|
||||||
|
if (firstNode.type === 'heading') {
|
||||||
|
firstNode.type = 'title';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = json.content;
|
||||||
|
|
||||||
|
const result = { type: 'doc', content: [] };
|
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; ) {
|
||||||
|
const node = nodes[i];
|
||||||
|
|
||||||
|
if (node.type === 'tableRow') {
|
||||||
|
const nextNode = nodes[i + 1];
|
||||||
|
|
||||||
|
if (nextNode && nextNode.type === 'table') {
|
||||||
|
nextNode.content.unshift(node);
|
||||||
|
result.content.push(nextNode);
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
// 出错了!!
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.content.push(node);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
// const state = ProseMirrorDOMParser.fromSchema(schema).parse(body);
|
||||||
|
// return state.toJSON();
|
||||||
},
|
},
|
||||||
|
|
||||||
// 将 ProseMirror JSONDocument 转换为 markdown 字符串
|
// 将 ProseMirror JSONDocument 转换为 markdown 字符串
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Mark } from './Mark';
|
||||||
|
|
||||||
|
export class Bold extends Mark {
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'STRONG';
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
type: 'bold',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Mark } from './Mark';
|
||||||
|
export class Code extends Mark {
|
||||||
|
matching() {
|
||||||
|
if (this.DOMNode.parentNode.nodeName === 'PRE') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.DOMNode.nodeName === 'CODE';
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
type: 'code',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Mark } from './Mark';
|
||||||
|
export class Italic extends Mark {
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'EM';
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
type: 'italic',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Mark } from './Mark';
|
||||||
|
export class Link extends Mark {
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'A';
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
type: 'link',
|
||||||
|
attrs: {
|
||||||
|
href: this.DOMNode.getAttribute('href'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export class Mark {
|
||||||
|
constructor(DomNode) {
|
||||||
|
this.type = 'mark';
|
||||||
|
this.DOMNode = DomNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class BulletList extends Node {
|
||||||
|
type = 'bulletList';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'UL';
|
||||||
|
}
|
||||||
|
|
||||||
|
// data() {
|
||||||
|
// return {
|
||||||
|
// type: 'bulletList',
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
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',
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
export class CodeBlockWrapper extends Node {
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'PRE';
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class HardBreak extends Node {
|
||||||
|
type = 'hardBreak';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'BR';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
export class Heading extends Node {
|
||||||
|
type = 'heading';
|
||||||
|
|
||||||
|
getLevel() {
|
||||||
|
const matches = this.DOMNode.nodeName.match(/^H([1-6])/);
|
||||||
|
return matches ? matches[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return Boolean(this.getLevel());
|
||||||
|
}
|
||||||
|
|
||||||
|
// data() {
|
||||||
|
// return {
|
||||||
|
// type: 'heading',
|
||||||
|
// attrs: {
|
||||||
|
// level: this.getLevel(),
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class Image extends Node {
|
||||||
|
type = 'image';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'IMG';
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
type: 'image',
|
||||||
|
attrs: {
|
||||||
|
src: this.DOMNode.getAttribute('src'),
|
||||||
|
class: this.DOMNode.getAttribute('class') || undefined,
|
||||||
|
alt: this.DOMNode.getAttribute('alt') || undefined,
|
||||||
|
title: this.DOMNode.getAttribute('title') || undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { getAttributes } from '../utils';
|
||||||
|
|
||||||
|
export class Node {
|
||||||
|
wrapper: null;
|
||||||
|
type = 'node';
|
||||||
|
DOMNode: HTMLElement;
|
||||||
|
|
||||||
|
constructor(DomNode: HTMLElement) {
|
||||||
|
this.wrapper = null;
|
||||||
|
this.DOMNode = DomNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
type: this.type,
|
||||||
|
attrs: getAttributes(this.type, this.DOMNode),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class OrderedList extends Node {
|
||||||
|
type = 'orderedList';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'OL';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
export class Paragraph extends Node {
|
||||||
|
type = 'paragraph';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'P';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
export class Text extends Node {
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === '#text';
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const text = this.DOMNode.nodeValue.replace(/^[\n]+/g, '');
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class Blockquote extends Node {
|
||||||
|
type = 'blockquote';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'BLOCKQUOTE';
|
||||||
|
}
|
||||||
|
|
||||||
|
// data() {
|
||||||
|
// return {
|
||||||
|
// type: 'blockquote',
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class Table extends Node {
|
||||||
|
type = 'table';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'TBODY' && this.DOMNode.parentNode.nodeName === 'TABLE';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class TableCell extends Node {
|
||||||
|
type = 'tableCell';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'TD';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class TableHeader extends Node {
|
||||||
|
type = 'tableHeader';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'TH';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class TableRow extends Node {
|
||||||
|
type = 'tableRow';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'TR';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class TaskList extends Node {
|
||||||
|
type = 'taskList';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'UL' && this.DOMNode.classList.contains('task-list');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Node } from './Node';
|
||||||
|
|
||||||
|
export class TaskListItem extends Node {
|
||||||
|
type = 'taskItem';
|
||||||
|
|
||||||
|
matching() {
|
||||||
|
return this.DOMNode.nodeName === 'LI' && this.DOMNode.classList.contains('task-list-item');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
import { BulletList } from './Nodes/BulletList';
|
||||||
|
import { CodeBlock } from './Nodes/CodeBlock';
|
||||||
|
import { CodeBlockWrapper } from './Nodes/CodeBlockWrapper';
|
||||||
|
import { HardBreak } from './Nodes/HardBreak';
|
||||||
|
import { Heading } from './Nodes/Heading';
|
||||||
|
import { Image } from './Nodes/Image';
|
||||||
|
import { ListItem } from './Nodes/ListItem';
|
||||||
|
import { OrderedList } from './Nodes/OrderedList';
|
||||||
|
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 { TableRow } from './Nodes/tableRow';
|
||||||
|
import { TableCell } from './Nodes/tableCell';
|
||||||
|
|
||||||
|
import { TaskList } from './Nodes/taskList';
|
||||||
|
import { TaskListItem } from './Nodes/taskListItem';
|
||||||
|
|
||||||
|
import { Bold } from './Marks/Bold';
|
||||||
|
import { Code } from './Marks/Code';
|
||||||
|
import { Italic } from './Marks/Italic';
|
||||||
|
import { Link } from './Marks/Link';
|
||||||
|
|
||||||
|
export class Renderer {
|
||||||
|
constructor() {
|
||||||
|
this.document = undefined;
|
||||||
|
this.storedMarks = [];
|
||||||
|
|
||||||
|
this.nodes = [
|
||||||
|
CodeBlock,
|
||||||
|
CodeBlockWrapper,
|
||||||
|
HardBreak,
|
||||||
|
Heading,
|
||||||
|
Image,
|
||||||
|
Paragraph,
|
||||||
|
Text,
|
||||||
|
Blockquote,
|
||||||
|
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
|
||||||
|
// 列表
|
||||||
|
TaskList,
|
||||||
|
TaskListItem,
|
||||||
|
OrderedList,
|
||||||
|
ListItem,
|
||||||
|
BulletList,
|
||||||
|
];
|
||||||
|
|
||||||
|
this.marks = [Bold, Code, Italic, Link];
|
||||||
|
}
|
||||||
|
|
||||||
|
setDocument(document) {
|
||||||
|
this.document = document;
|
||||||
|
}
|
||||||
|
|
||||||
|
stripWhitespace(value) {
|
||||||
|
// return minify(value, {
|
||||||
|
// collapseWhitespace: true,
|
||||||
|
// });
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDocumentBody() {
|
||||||
|
return this.document;
|
||||||
|
// return this.document.window.document.querySelector('body');
|
||||||
|
}
|
||||||
|
|
||||||
|
render(value) {
|
||||||
|
this.setDocument(value);
|
||||||
|
|
||||||
|
console.log(value);
|
||||||
|
|
||||||
|
const content = this.renderChildren(this.getDocumentBody());
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'doc',
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChildren(node) {
|
||||||
|
let nodes = [];
|
||||||
|
|
||||||
|
node.childNodes.forEach((child) => {
|
||||||
|
const NodeClass = this.getMatchingNode(child);
|
||||||
|
let MarkClass;
|
||||||
|
|
||||||
|
if (NodeClass) {
|
||||||
|
let item = NodeClass.data();
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
if (child.hasChildNodes()) {
|
||||||
|
nodes.push(...this.renderChildren(child));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.hasChildNodes()) {
|
||||||
|
item = {
|
||||||
|
...item,
|
||||||
|
content: this.renderChildren(child),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.storedMarks.length) {
|
||||||
|
item = {
|
||||||
|
...item,
|
||||||
|
marks: this.storedMarks,
|
||||||
|
};
|
||||||
|
this.storedMarks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NodeClass.wrapper) {
|
||||||
|
item.content = [
|
||||||
|
{
|
||||||
|
...NodeClass.wrapper,
|
||||||
|
content: item.content || [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push(item);
|
||||||
|
} else if ((MarkClass = this.getMatchingMark(child))) {
|
||||||
|
this.storedMarks.push(MarkClass.data());
|
||||||
|
|
||||||
|
if (child.hasChildNodes()) {
|
||||||
|
nodes.push(...this.renderChildren(child));
|
||||||
|
}
|
||||||
|
} else if (child.hasChildNodes()) {
|
||||||
|
nodes.push(...this.renderChildren(child));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMatchingNode(item) {
|
||||||
|
return this.getMatchingClass(item, this.nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMatchingMark(item) {
|
||||||
|
return this.getMatchingClass(item, this.marks);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMatchingClass(node, classes) {
|
||||||
|
for (let i in classes) {
|
||||||
|
const Class = classes[i];
|
||||||
|
const instance = new Class(node);
|
||||||
|
// console.log(node);
|
||||||
|
if (instance.matching()) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNode(node) {
|
||||||
|
this.nodes.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
addNodes(nodes) {
|
||||||
|
for (const i in nodes) {
|
||||||
|
this.addNode(nodes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMark(mark) {
|
||||||
|
this.marks.push(mark);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMarks(marks) {
|
||||||
|
for (const i in marks) {
|
||||||
|
this.addMark(marks[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
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;
|
||||||
|
};
|
Loading…
Reference in New Issue