chore: Move editor into codebase (#2930)
This commit is contained in:
42
shared/editor/marks/Bold.ts
Normal file
42
shared/editor/marks/Bold.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { toggleMark } from "prosemirror-commands";
|
||||
import { InputRule } from "prosemirror-inputrules";
|
||||
import { MarkSpec, MarkType } from "prosemirror-model";
|
||||
import markInputRule from "../lib/markInputRule";
|
||||
import Mark from "./Mark";
|
||||
|
||||
export default class Bold extends Mark {
|
||||
get name() {
|
||||
return "strong";
|
||||
}
|
||||
|
||||
get schema(): MarkSpec {
|
||||
return {
|
||||
parseDOM: [{ tag: "b" }, { tag: "strong" }],
|
||||
toDOM: () => ["strong"],
|
||||
};
|
||||
}
|
||||
|
||||
inputRules({ type }: { type: MarkType }): InputRule[] {
|
||||
return [markInputRule(/(?:\*\*)([^*]+)(?:\*\*)$/, type)];
|
||||
}
|
||||
|
||||
keys({ type }: { type: MarkType }) {
|
||||
return {
|
||||
"Mod-b": toggleMark(type),
|
||||
"Mod-B": toggleMark(type),
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown() {
|
||||
return {
|
||||
open: "**",
|
||||
close: "**",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
};
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return { mark: "strong" };
|
||||
}
|
||||
}
|
||||
87
shared/editor/marks/Code.ts
Normal file
87
shared/editor/marks/Code.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { toggleMark } from "prosemirror-commands";
|
||||
import {
|
||||
MarkSpec,
|
||||
MarkType,
|
||||
Node as ProsemirrorNode,
|
||||
Mark as ProsemirrorMark,
|
||||
} from "prosemirror-model";
|
||||
import moveLeft from "../commands/moveLeft";
|
||||
import moveRight from "../commands/moveRight";
|
||||
import markInputRule from "../lib/markInputRule";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import Mark from "./Mark";
|
||||
|
||||
function backticksFor(node: ProsemirrorNode, side: -1 | 1) {
|
||||
const ticks = /`+/g;
|
||||
let match: RegExpMatchArray | null;
|
||||
let len = 0;
|
||||
|
||||
if (node.isText) {
|
||||
while ((match = ticks.exec(node.text || ""))) {
|
||||
len = Math.max(len, match[0].length);
|
||||
}
|
||||
}
|
||||
|
||||
let result = len > 0 && side > 0 ? " `" : "`";
|
||||
for (let i = 0; i < len; i++) {
|
||||
result += "`";
|
||||
}
|
||||
if (len > 0 && side < 0) {
|
||||
result += " ";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export default class Code extends Mark {
|
||||
get name() {
|
||||
return "code_inline";
|
||||
}
|
||||
|
||||
get schema(): MarkSpec {
|
||||
return {
|
||||
excludes: "_",
|
||||
parseDOM: [{ tag: "code", preserveWhitespace: true }],
|
||||
toDOM: () => ["code", { spellCheck: "false" }],
|
||||
};
|
||||
}
|
||||
|
||||
inputRules({ type }: { type: MarkType }) {
|
||||
return [markInputRule(/(?:^|[^`])(`([^`]+)`)$/, type)];
|
||||
}
|
||||
|
||||
keys({ type }: { type: MarkType }) {
|
||||
// Note: This key binding only works on non-Mac platforms
|
||||
// https://github.com/ProseMirror/prosemirror/issues/515
|
||||
return {
|
||||
"Mod`": toggleMark(type),
|
||||
ArrowLeft: moveLeft(),
|
||||
ArrowRight: moveRight(),
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown() {
|
||||
return {
|
||||
open(
|
||||
_state: MarkdownSerializerState,
|
||||
_mark: ProsemirrorMark,
|
||||
parent: ProsemirrorNode,
|
||||
index: number
|
||||
) {
|
||||
return backticksFor(parent.child(index), -1);
|
||||
},
|
||||
close(
|
||||
_state: MarkdownSerializerState,
|
||||
_mark: ProsemirrorMark,
|
||||
parent: ProsemirrorNode,
|
||||
index: number
|
||||
) {
|
||||
return backticksFor(parent.child(index - 1), 1);
|
||||
},
|
||||
escape: false,
|
||||
};
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return { mark: "code_inline" };
|
||||
}
|
||||
}
|
||||
45
shared/editor/marks/Highlight.ts
Normal file
45
shared/editor/marks/Highlight.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { toggleMark } from "prosemirror-commands";
|
||||
import { MarkSpec, MarkType } from "prosemirror-model";
|
||||
import markInputRule from "../lib/markInputRule";
|
||||
import markRule from "../rules/mark";
|
||||
import Mark from "./Mark";
|
||||
|
||||
export default class Highlight extends Mark {
|
||||
get name() {
|
||||
return "highlight";
|
||||
}
|
||||
|
||||
get schema(): MarkSpec {
|
||||
return {
|
||||
parseDOM: [{ tag: "mark" }],
|
||||
toDOM: () => ["mark"],
|
||||
};
|
||||
}
|
||||
|
||||
inputRules({ type }: { type: MarkType }) {
|
||||
return [markInputRule(/(?:==)([^=]+)(?:==)$/, type)];
|
||||
}
|
||||
|
||||
keys({ type }: { type: MarkType }) {
|
||||
return {
|
||||
"Mod-Ctrl-h": toggleMark(type),
|
||||
};
|
||||
}
|
||||
|
||||
get rulePlugins() {
|
||||
return [markRule({ delim: "==", mark: "highlight" })];
|
||||
}
|
||||
|
||||
toMarkdown() {
|
||||
return {
|
||||
open: "==",
|
||||
close: "==",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
};
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return { mark: "highlight" };
|
||||
}
|
||||
}
|
||||
46
shared/editor/marks/Italic.ts
Normal file
46
shared/editor/marks/Italic.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { toggleMark } from "prosemirror-commands";
|
||||
import { InputRule } from "prosemirror-inputrules";
|
||||
import { MarkSpec, MarkType } from "prosemirror-model";
|
||||
import { Command } from "../lib/Extension";
|
||||
import markInputRule from "../lib/markInputRule";
|
||||
import Mark from "./Mark";
|
||||
|
||||
export default class Italic extends Mark {
|
||||
get name() {
|
||||
return "em";
|
||||
}
|
||||
|
||||
get schema(): MarkSpec {
|
||||
return {
|
||||
parseDOM: [{ tag: "i" }, { tag: "em" }],
|
||||
toDOM: () => ["em"],
|
||||
};
|
||||
}
|
||||
|
||||
inputRules({ type }: { type: MarkType }): InputRule[] {
|
||||
return [
|
||||
markInputRule(/(?:^|[\s])(_([^_]+)_)$/, type),
|
||||
markInputRule(/(?:^|[^*])(\*([^*]+)\*)$/, type),
|
||||
];
|
||||
}
|
||||
|
||||
keys({ type }: { type: MarkType }): Record<string, Command> {
|
||||
return {
|
||||
"Mod-i": toggleMark(type),
|
||||
"Mod-I": toggleMark(type),
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown() {
|
||||
return {
|
||||
open: "*",
|
||||
close: "*",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
};
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return { mark: "em" };
|
||||
}
|
||||
}
|
||||
194
shared/editor/marks/Link.ts
Normal file
194
shared/editor/marks/Link.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import Token from "markdown-it/lib/token";
|
||||
import { toggleMark } from "prosemirror-commands";
|
||||
import { InputRule } from "prosemirror-inputrules";
|
||||
import { MarkdownSerializerState } from "prosemirror-markdown";
|
||||
import {
|
||||
MarkSpec,
|
||||
MarkType,
|
||||
Node,
|
||||
Mark as ProsemirrorMark,
|
||||
} from "prosemirror-model";
|
||||
import { Transaction, EditorState, Plugin } from "prosemirror-state";
|
||||
import Mark from "./Mark";
|
||||
|
||||
const LINK_INPUT_REGEX = /\[([^[]+)]\((\S+)\)$/;
|
||||
|
||||
function isPlainURL(
|
||||
link: ProsemirrorMark,
|
||||
parent: Node,
|
||||
index: number,
|
||||
side: -1 | 1
|
||||
) {
|
||||
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = parent.child(index + (side < 0 ? -1 : 0));
|
||||
if (
|
||||
!content.isText ||
|
||||
content.text !== link.attrs.href ||
|
||||
content.marks[content.marks.length - 1] !== link
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index === (side < 0 ? 1 : parent.childCount - 1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const next = parent.child(index + (side < 0 ? -2 : 1));
|
||||
return !link.isInSet(next.marks);
|
||||
}
|
||||
|
||||
export default class Link extends Mark {
|
||||
get name() {
|
||||
return "link";
|
||||
}
|
||||
|
||||
get schema(): MarkSpec {
|
||||
return {
|
||||
attrs: {
|
||||
href: {
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: "a[href]",
|
||||
getAttrs: (dom: HTMLElement) => ({
|
||||
href: dom.getAttribute("href"),
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: (node) => [
|
||||
"a",
|
||||
{
|
||||
...node.attrs,
|
||||
rel: "noopener noreferrer nofollow",
|
||||
},
|
||||
0,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
inputRules({ type }: { type: MarkType }) {
|
||||
return [
|
||||
new InputRule(LINK_INPUT_REGEX, (state, match, start, end) => {
|
||||
const [okay, alt, href] = match;
|
||||
const { tr } = state;
|
||||
|
||||
if (okay) {
|
||||
tr.replaceWith(start, end, this.editor.schema.text(alt)).addMark(
|
||||
start,
|
||||
start + alt.length,
|
||||
type.create({ href })
|
||||
);
|
||||
}
|
||||
|
||||
return tr;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
commands({ type }: { type: MarkType }) {
|
||||
return ({ href } = { href: "" }) => toggleMark(type, { href });
|
||||
}
|
||||
|
||||
keys({ type }: { type: MarkType }) {
|
||||
return {
|
||||
"Mod-k": (state: EditorState, dispatch: (tr: Transaction) => void) => {
|
||||
if (state.selection.empty) {
|
||||
this.options.onKeyboardShortcut();
|
||||
return true;
|
||||
}
|
||||
|
||||
return toggleMark(type, { href: "" })(state, dispatch);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
mouseover: (_view, event: MouseEvent) => {
|
||||
if (
|
||||
event.target instanceof HTMLAnchorElement &&
|
||||
!event.target.className.includes("ProseMirror-widget")
|
||||
) {
|
||||
if (this.options.onHoverLink) {
|
||||
return this.options.onHoverLink(event);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
click: (_view, event: MouseEvent) => {
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
const href =
|
||||
event.target.href ||
|
||||
(event.target.parentNode instanceof HTMLAnchorElement
|
||||
? event.target.parentNode.href
|
||||
: "");
|
||||
|
||||
const isHashtag = href.startsWith("#");
|
||||
if (isHashtag && this.options.onClickHashtag) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.options.onClickHashtag(href, event);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.options.onClickLink) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.options.onClickLink(href, event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
toMarkdown() {
|
||||
return {
|
||||
open(
|
||||
_state: MarkdownSerializerState,
|
||||
mark: ProsemirrorMark,
|
||||
parent: Node,
|
||||
index: number
|
||||
) {
|
||||
return isPlainURL(mark, parent, index, 1) ? "<" : "[";
|
||||
},
|
||||
close(
|
||||
state: MarkdownSerializerState,
|
||||
mark: ProsemirrorMark,
|
||||
parent: Node,
|
||||
index: number
|
||||
) {
|
||||
return isPlainURL(mark, parent, index, -1)
|
||||
? ">"
|
||||
: "](" +
|
||||
state.esc(mark.attrs.href) +
|
||||
(mark.attrs.title ? " " + state.quote(mark.attrs.title) : "") +
|
||||
")";
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return {
|
||||
mark: "link",
|
||||
getAttrs: (tok: Token) => ({
|
||||
href: tok.attrGet("href"),
|
||||
title: tok.attrGet("title") || null,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
45
shared/editor/marks/Mark.ts
Normal file
45
shared/editor/marks/Mark.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { toggleMark } from "prosemirror-commands";
|
||||
import { InputRule } from "prosemirror-inputrules";
|
||||
import { TokenConfig } from "prosemirror-markdown";
|
||||
import {
|
||||
MarkSpec,
|
||||
MarkType,
|
||||
Node as ProsemirrorNode,
|
||||
Schema,
|
||||
} from "prosemirror-model";
|
||||
import Extension, { Command, CommandFactory } from "../lib/Extension";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
|
||||
export default abstract class Mark extends Extension {
|
||||
get type() {
|
||||
return "mark";
|
||||
}
|
||||
|
||||
get schema(): MarkSpec {
|
||||
return {};
|
||||
}
|
||||
|
||||
get markdownToken(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
keys(_options: { type: MarkType; schema: Schema }): Record<string, Command> {
|
||||
return {};
|
||||
}
|
||||
|
||||
inputRules(_options: { type: MarkType; schema: Schema }): InputRule[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
|
||||
console.error("toMarkdown not implemented", state, node);
|
||||
}
|
||||
|
||||
parseMarkdown(): TokenConfig | void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
commands({ type }: { type: MarkType; schema: Schema }): CommandFactory {
|
||||
return () => toggleMark(type);
|
||||
}
|
||||
}
|
||||
163
shared/editor/marks/Placeholder.ts
Normal file
163
shared/editor/marks/Placeholder.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { MarkSpec } from "prosemirror-model";
|
||||
import { Plugin, TextSelection } from "prosemirror-state";
|
||||
import getMarkRange from "../queries/getMarkRange";
|
||||
import markRule from "../rules/mark";
|
||||
import Mark from "./Mark";
|
||||
|
||||
export default class Placeholder extends Mark {
|
||||
get name() {
|
||||
return "placeholder";
|
||||
}
|
||||
|
||||
get schema(): MarkSpec {
|
||||
return {
|
||||
parseDOM: [{ tag: "span.template-placeholder" }],
|
||||
toDOM: () => ["span", { class: "template-placeholder" }],
|
||||
};
|
||||
}
|
||||
|
||||
get rulePlugins() {
|
||||
return [markRule({ delim: "!!", mark: "placeholder" })];
|
||||
}
|
||||
|
||||
toMarkdown() {
|
||||
return {
|
||||
open: "!!",
|
||||
close: "!!",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
};
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return { mark: "placeholder" };
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
props: {
|
||||
handleTextInput: (view, from, to, text) => {
|
||||
if (this.editor.props.template) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { state, dispatch } = view;
|
||||
const $from = state.doc.resolve(from);
|
||||
|
||||
const range = getMarkRange($from, state.schema.marks.placeholder);
|
||||
if (!range) return false;
|
||||
|
||||
const selectionStart = Math.min(from, range.from);
|
||||
const selectionEnd = Math.max(to, range.to);
|
||||
|
||||
dispatch(
|
||||
state.tr
|
||||
.removeMark(
|
||||
range.from,
|
||||
range.to,
|
||||
state.schema.marks.placeholder
|
||||
)
|
||||
.insertText(text, selectionStart, selectionEnd)
|
||||
);
|
||||
|
||||
const $to = view.state.doc.resolve(selectionStart + text.length);
|
||||
dispatch(view.state.tr.setSelection(TextSelection.near($to)));
|
||||
|
||||
return true;
|
||||
},
|
||||
handleKeyDown: (view, event: KeyboardEvent) => {
|
||||
if (!view.props.editable || !view.props.editable(view.state)) {
|
||||
return false;
|
||||
}
|
||||
if (this.editor.props.template) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
event.key !== "ArrowLeft" &&
|
||||
event.key !== "ArrowRight" &&
|
||||
event.key !== "Backspace"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { state, dispatch } = view;
|
||||
|
||||
if (event.key === "Backspace") {
|
||||
const range = getMarkRange(
|
||||
state.doc.resolve(Math.max(0, state.selection.from - 1)),
|
||||
state.schema.marks.placeholder
|
||||
);
|
||||
if (!range) return false;
|
||||
|
||||
dispatch(
|
||||
state.tr
|
||||
.removeMark(
|
||||
range.from,
|
||||
range.to,
|
||||
state.schema.marks.placeholder
|
||||
)
|
||||
.insertText("", range.from, range.to)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowLeft") {
|
||||
const range = getMarkRange(
|
||||
state.doc.resolve(Math.max(0, state.selection.from - 1)),
|
||||
state.schema.marks.placeholder
|
||||
);
|
||||
if (!range) return false;
|
||||
|
||||
const startOfMark = state.doc.resolve(range.from);
|
||||
dispatch(state.tr.setSelection(TextSelection.near(startOfMark)));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowRight") {
|
||||
const range = getMarkRange(
|
||||
state.selection.$from,
|
||||
state.schema.marks.placeholder
|
||||
);
|
||||
if (!range) return false;
|
||||
|
||||
const endOfMark = state.doc.resolve(range.to);
|
||||
dispatch(state.tr.setSelection(TextSelection.near(endOfMark)));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
handleClick: (view, pos, event: MouseEvent) => {
|
||||
if (!view.props.editable || !view.props.editable(view.state)) {
|
||||
return false;
|
||||
}
|
||||
if (this.editor.props.template) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
event.target instanceof HTMLSpanElement &&
|
||||
event.target.className.includes("template-placeholder")
|
||||
) {
|
||||
const { state, dispatch } = view;
|
||||
const range = getMarkRange(
|
||||
state.selection.$from,
|
||||
state.schema.marks.placeholder
|
||||
);
|
||||
if (!range) return false;
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const startOfMark = state.doc.resolve(range.from);
|
||||
dispatch(state.tr.setSelection(TextSelection.near(startOfMark)));
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
54
shared/editor/marks/Strikethrough.ts
Normal file
54
shared/editor/marks/Strikethrough.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { toggleMark } from "prosemirror-commands";
|
||||
import { MarkSpec, MarkType } from "prosemirror-model";
|
||||
import markInputRule from "../lib/markInputRule";
|
||||
import Mark from "./Mark";
|
||||
|
||||
export default class Strikethrough extends Mark {
|
||||
get name() {
|
||||
return "strikethrough";
|
||||
}
|
||||
|
||||
get schema(): MarkSpec {
|
||||
return {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: "s",
|
||||
},
|
||||
{
|
||||
tag: "del",
|
||||
},
|
||||
{
|
||||
tag: "strike",
|
||||
},
|
||||
],
|
||||
toDOM: () => ["del", 0],
|
||||
};
|
||||
}
|
||||
|
||||
keys({ type }: { type: MarkType }) {
|
||||
return {
|
||||
"Mod-d": toggleMark(type),
|
||||
};
|
||||
}
|
||||
|
||||
inputRules({ type }: { type: MarkType }) {
|
||||
return [markInputRule(/~([^~]+)~$/, type)];
|
||||
}
|
||||
|
||||
toMarkdown() {
|
||||
return {
|
||||
open: "~~",
|
||||
close: "~~",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
};
|
||||
}
|
||||
|
||||
get markdownToken() {
|
||||
return "s";
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return { mark: "strikethrough" };
|
||||
}
|
||||
}
|
||||
51
shared/editor/marks/Underline.ts
Normal file
51
shared/editor/marks/Underline.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { toggleMark } from "prosemirror-commands";
|
||||
import { MarkSpec, MarkType } from "prosemirror-model";
|
||||
import markInputRule from "../lib/markInputRule";
|
||||
import underlinesRule from "../rules/underlines";
|
||||
import Mark from "./Mark";
|
||||
|
||||
export default class Underline extends Mark {
|
||||
get name() {
|
||||
return "underline";
|
||||
}
|
||||
|
||||
get schema(): MarkSpec {
|
||||
return {
|
||||
parseDOM: [
|
||||
{ tag: "u" },
|
||||
{
|
||||
style: "text-decoration",
|
||||
getAttrs: (value) => (value === "underline" ? {} : undefined),
|
||||
},
|
||||
],
|
||||
toDOM: () => ["u", 0],
|
||||
};
|
||||
}
|
||||
|
||||
get rulePlugins() {
|
||||
return [underlinesRule];
|
||||
}
|
||||
|
||||
inputRules({ type }: { type: MarkType }) {
|
||||
return [markInputRule(/(?:__)([^_]+)(?:__)$/, type)];
|
||||
}
|
||||
|
||||
keys({ type }: { type: MarkType }) {
|
||||
return {
|
||||
"Mod-u": toggleMark(type),
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown() {
|
||||
return {
|
||||
open: "__",
|
||||
close: "__",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
};
|
||||
}
|
||||
|
||||
parseMarkdown() {
|
||||
return { mark: "underline" };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user