@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
197 lines (180 loc) • 6.07 kB
text/typescript
import {
Plugin,
PluginKey,
EditorView,
Slice,
MarkdownParser,
Schema,
} from '../../prosemirror';
import * as MarkdownIt from 'markdown-it';
import table from 'markdown-it-table';
import { stateKey as tableStateKey } from '../table';
import { containsTable } from '../table/utils';
import { isSingleLine } from './util';
function isCode(str) {
const lines = str.split(/\r?\n|\r/);
if (3 > lines.length) {
return false;
}
let weight = 0;
lines.forEach(line => {
// Ends with : or ;
if (/[:;]$/.test(line)) { weight++; }
// Contains second and third braces
if (/[{}\[\]]/.test(line)) { weight++; }
// Contains <tag> or </
if ((/<\w+>/.test(line) || /<\//.test(line))) { weight++; }
// Contains () <- function calls
if (/\(\)/.test(line)) { weight++; }
// New line starts with less than two chars. e.g- if, {, <, etc
const token = /^(\s+)[a-zA-Z<{]{2,}/.exec(line);
if (token && 2 <= token[1].length) { weight++; }
if (/&&/.test(line)) { weight++; }
});
return 4 <= weight && weight >= 0.5 * lines.length;
}
export const stateKey = new PluginKey('pastePlugin');
const pmSchemaToMdMapping = {
nodes: {
blockquote: 'blockquote',
paragraph: 'paragraph',
rule: 'hr',
// lheading (---, ===)
heading: ['heading', 'lheading'],
codeBlock: ['code', 'fence'],
listItem: 'list',
image: 'image',
},
marks: {
em: 'emphasis',
strong: 'text',
link: ['link', 'autolink', 'reference', 'linkify'],
strike: 'strikethrough',
code: 'backticks',
}
};
export const createPlugin = (schema: Schema<any, any>) => {
let atlassianMarkDownParser: MarkdownParser;
const md = MarkdownIt('zero', { html: false, linkify: true });
md.enable([
// Process html entity - {, ¯, ", ...
'entity',
// Process escaped chars and hardbreaks
'escape'
]);
// Enable markdown plugins based on schema
['nodes', 'marks'].forEach(key => {
for (const idx in pmSchemaToMdMapping[key]) {
if (schema[key][idx]) {
md.enable(pmSchemaToMdMapping[key][idx]);
}
}
});
if (schema.nodes.table) {
md.use(table);
}
const filterMdToPmSchemaMapping = map => Object.keys(map).reduce((newMap, key) => {
const value = map[key];
const block = value.block || value.node;
const mark = value.mark;
if ((block && schema.nodes[block]) || (mark && schema.marks[mark])) {
newMap[key] = value;
}
return newMap;
}, {});
atlassianMarkDownParser = new MarkdownParser(schema, md, filterMdToPmSchemaMapping({
blockquote: { block: 'blockquote' },
paragraph: { block: 'paragraph' },
em: { mark: 'em' },
strong: { mark: 'strong' },
link: {
mark: 'link', attrs: tok => ({
href: tok.attrGet('href'),
title: tok.attrGet('title') || null
})
},
hr: { node: 'rule' },
heading: { block: 'heading', attrs: tok => ({ level: +tok.tag.slice(1) }) },
code_block: { block: 'codeBlock' },
list_item: { block: 'listItem' },
bullet_list: { block: 'bulletList' },
ordered_list: { block: 'orderedList', attrs: tok => ({ order: +tok.attrGet('order') || 1 }) },
code_inline: { mark: 'code' },
fence: { block: 'codeBlock', attrs: tok => ({ language: tok.info || '' }) },
image: {
node: 'image', attrs: tok => ({
src: tok.attrGet('src'),
title: tok.attrGet('title') || null,
alt: tok.children[0] && tok.children[0].content || null,
})
},
emoji: {
node: 'emoji', attrs: tok => ({
shortName: `:${tok.markup}:`,
text: tok.content,
})
},
table: { block: 'table' },
tr: { block: 'tableRow' },
th: { block: 'tableHeader' },
td: { block: 'tableCell' },
s: { mark: 'strike' },
}));
return new Plugin({
key: stateKey,
props: {
handlePaste(view: EditorView, event: ClipboardEvent, slice: Slice) {
if (!event.clipboardData) {
return false;
}
const text = event.clipboardData.getData('text/plain');
const html = event.clipboardData.getData('text/html');
const node = slice.content.firstChild;
const { schema } = view.state;
const { $from } = view.state.selection;
const selectedNode = $from.node($from.depth);
if (text && selectedNode.type === schema.nodes.codeBlock) {
view.dispatch(view.state.tr.insertText(text));
return true;
}
if (
(text && isCode(text)) ||
(text && html && node && node.type === schema.nodes.codeBlock)
) {
let tr;
if (isSingleLine(text)) {
tr = view.state.tr.insertText(text);
tr = tr.addMark($from.pos, $from.pos + text.length, schema.marks.code.create());
} else {
const codeBlockNode = schema.nodes.codeBlock.create(node ? node.attrs : {}, schema.text(text));
tr = view.state.tr.replaceSelectionWith(codeBlockNode);
}
view.dispatch(tr.scrollIntoView());
return true;
}
if (text && !html && atlassianMarkDownParser) {
const doc = atlassianMarkDownParser.parse(text);
if (doc && doc.content) {
const tr = view.state.tr.replaceSelection(
new Slice(doc.content, slice.openStart, slice.openEnd)
);
view.dispatch(tr.scrollIntoView());
return true;
}
}
if (html) {
const tableState = tableStateKey.getState(view.state);
if (tableState && tableState.isRequiredToAddHeader() && containsTable(view, slice)) {
const { state, dispatch } = view;
const selectionStart = state.selection.$from.pos;
dispatch(state.tr.replaceSelection(slice));
tableState.addHeaderToTableNodes(slice, selectionStart);
return true;
}
}
return false;
},
}
});
};
export default (schema: Schema<any, any>) => [createPlugin(schema)];