The upstream TipTap Code extension input rule regex /(^|[^`])`([^`]+)`(?!`)$/ uses a capture group (^|[^`]) that includes the character preceding the opening backtick in the full match. When markInputRule processes this, it deletes everything from the match start to the code content, which removes that preceding character along with the backtick delimiters. For example, typing foo(`bar` would result in foo`bar` (formatted) instead of the expected foo(`bar` (formatted) — the ( is lost. Fix: disable the built-in Code extension from StarterKit and register it separately with a corrected regex that uses a lookbehind assertion (?:^|(?<=[^`])) instead of a capture group. The lookbehind asserts the preceding character without including it in the match, so markInputRule only deletes the backtick delimiters. Functionally tested on Firefox and Chrome. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
352 lines
10 KiB
TypeScript
352 lines
10 KiB
TypeScript
import { markInputRule } from "@tiptap/core";
|
|
import { StarterKit } from "@tiptap/starter-kit";
|
|
import { Code } from "@tiptap/extension-code";
|
|
import { TextAlign } from "@tiptap/extension-text-align";
|
|
import { TaskList, TaskItem } from "@tiptap/extension-list";
|
|
import { Placeholder, CharacterCount } from "@tiptap/extensions";
|
|
import { Superscript } from "@tiptap/extension-superscript";
|
|
import SubScript from "@tiptap/extension-subscript";
|
|
import { Typography } from "@tiptap/extension-typography";
|
|
import { TextStyle } from "@tiptap/extension-text-style";
|
|
import { Color } from "@tiptap/extension-color";
|
|
import GlobalDragHandle from "tiptap-extension-global-drag-handle";
|
|
import { Youtube } from "@tiptap/extension-youtube";
|
|
import SlashCommand from "@/features/editor/extensions/slash-command";
|
|
import { Collaboration, isChangeOrigin } from "@tiptap/extension-collaboration";
|
|
import { CollaborationCaret } from "@tiptap/extension-collaboration-caret";
|
|
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
import {
|
|
Comment,
|
|
Details,
|
|
DetailsContent,
|
|
DetailsSummary,
|
|
MathBlock,
|
|
MathInline,
|
|
TableCell,
|
|
TableRow,
|
|
TableHeader,
|
|
CustomTable,
|
|
TrailingNode,
|
|
TiptapImage,
|
|
Callout,
|
|
TiptapVideo,
|
|
LinkExtension,
|
|
Selection,
|
|
Attachment,
|
|
CustomCodeBlock,
|
|
Drawio,
|
|
Excalidraw,
|
|
Embed,
|
|
SearchAndReplace,
|
|
Mention,
|
|
TableDndExtension,
|
|
Subpages,
|
|
Heading,
|
|
Highlight,
|
|
UniqueID,
|
|
SharedStorage,
|
|
Columns,
|
|
Column,
|
|
} from "@docmost/editor-ext";
|
|
import {
|
|
randomElement,
|
|
userColors,
|
|
} from "@/features/editor/extensions/utils.ts";
|
|
import { IUser } from "@/features/user/types/user.types.ts";
|
|
import MathInlineView from "@/features/editor/components/math/math-inline.tsx";
|
|
import MathBlockView from "@/features/editor/components/math/math-block.tsx";
|
|
import ImageView from "@/features/editor/components/image/image-view.tsx";
|
|
import {
|
|
createImageHandle,
|
|
imageResizeClasses,
|
|
} from "@/features/editor/components/image/image-resize-handles.ts";
|
|
import {
|
|
createResizeHandle,
|
|
buildResizeClasses,
|
|
} from "@/features/editor/components/common/node-resize-handles.ts";
|
|
import CalloutView from "@/features/editor/components/callout/callout-view.tsx";
|
|
import VideoView from "@/features/editor/components/video/video-view.tsx";
|
|
import AttachmentView from "@/features/editor/components/attachment/attachment-view.tsx";
|
|
import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx";
|
|
import DrawioView from "../components/drawio/drawio-view";
|
|
import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx";
|
|
import EmbedView from "@/features/editor/components/embed/embed-view.tsx";
|
|
import SubpagesView from "@/features/editor/components/subpages/subpages-view.tsx";
|
|
import { common, createLowlight } from "lowlight";
|
|
import plaintext from "highlight.js/lib/languages/plaintext";
|
|
import powershell from "highlight.js/lib/languages/powershell";
|
|
import abap from "highlightjs-sap-abap";
|
|
import elixir from "highlight.js/lib/languages/elixir";
|
|
import erlang from "highlight.js/lib/languages/erlang";
|
|
import dockerfile from "highlight.js/lib/languages/dockerfile";
|
|
import clojure from "highlight.js/lib/languages/clojure";
|
|
import fortran from "highlight.js/lib/languages/fortran";
|
|
import haskell from "highlight.js/lib/languages/haskell";
|
|
import scala from "highlight.js/lib/languages/scala";
|
|
import mentionRenderItems from "@/features/editor/components/mention/mention-suggestion.ts";
|
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
|
import MentionView from "@/features/editor/components/mention/mention-view.tsx";
|
|
import i18n from "@/i18n.ts";
|
|
import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts";
|
|
import EmojiCommand from "./emoji-command";
|
|
import { countWords } from "alfaaz";
|
|
|
|
const lowlight = createLowlight(common);
|
|
lowlight.register("mermaid", plaintext);
|
|
lowlight.register("powershell", powershell);
|
|
lowlight.register("abap", abap);
|
|
lowlight.register("erlang", erlang);
|
|
lowlight.register("elixir", elixir);
|
|
lowlight.register("dockerfile", dockerfile);
|
|
lowlight.register("clojure", clojure);
|
|
lowlight.register("fortran", fortran);
|
|
lowlight.register("haskell", haskell);
|
|
lowlight.register("scala", scala);
|
|
|
|
// @ts-ignore
|
|
export const mainExtensions = [
|
|
StarterKit.configure({
|
|
heading: false,
|
|
undoRedo: false,
|
|
link: false,
|
|
trailingNode: false,
|
|
dropcursor: {
|
|
width: 3,
|
|
color: "#70CFF8",
|
|
},
|
|
codeBlock: false,
|
|
code: false,
|
|
}),
|
|
// Override TipTap's Code extension to fix the inline code input rule.
|
|
// The upstream regex /(^|[^`])`([^`]+)`(?!`)$/ captures the character
|
|
// before the opening backtick as part of the match, causing markInputRule
|
|
// to delete it. Using a lookbehind avoids including it in the match.
|
|
Code.configure({
|
|
HTMLAttributes: {
|
|
spellcheck: false,
|
|
},
|
|
}).extend({
|
|
addInputRules() {
|
|
return [
|
|
markInputRule({
|
|
find: /(?:^|(?<=[^`]))`([^`]+)`(?!`)$/,
|
|
type: this.type,
|
|
}),
|
|
];
|
|
},
|
|
}),
|
|
SharedStorage,
|
|
Heading,
|
|
UniqueID.configure({
|
|
types: ["heading", "paragraph"],
|
|
filterTransaction: (transaction) => !isChangeOrigin(transaction),
|
|
}),
|
|
Placeholder.configure({
|
|
placeholder: ({ editor, node, pos }) => {
|
|
if (node.type.name === "heading") {
|
|
return i18n.t("Heading {{level}}", { level: node.attrs.level });
|
|
}
|
|
if (node.type.name === "detailsSummary") {
|
|
return i18n.t("Toggle title");
|
|
}
|
|
if (node.type.name === "paragraph") {
|
|
const $pos = editor.state.doc.resolve(pos);
|
|
const parentName = $pos.parent.type.name;
|
|
if (
|
|
parentName === "column" ||
|
|
parentName === "tableCell" ||
|
|
parentName === "tableHeader" ||
|
|
parentName === "callout" ||
|
|
parentName === "blockquote"
|
|
) {
|
|
return i18n.t("Write...");
|
|
}
|
|
return i18n.t('Write anything. Enter "/" for commands');
|
|
}
|
|
},
|
|
includeChildren: true,
|
|
showOnlyWhenEditable: true,
|
|
}),
|
|
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
TaskList,
|
|
TaskItem.configure({
|
|
nested: true,
|
|
}),
|
|
LinkExtension.configure({
|
|
openOnClick: false,
|
|
}),
|
|
Superscript,
|
|
SubScript,
|
|
Highlight.configure({
|
|
multicolor: true,
|
|
}),
|
|
Typography,
|
|
TrailingNode,
|
|
GlobalDragHandle,
|
|
TextStyle,
|
|
Color,
|
|
SlashCommand,
|
|
EmojiCommand,
|
|
Comment.configure({
|
|
HTMLAttributes: {
|
|
class: "comment-mark",
|
|
},
|
|
}),
|
|
Mention.configure({
|
|
suggestion: {
|
|
allowSpaces: true,
|
|
items: () => {
|
|
return [];
|
|
},
|
|
// @ts-ignore
|
|
render: mentionRenderItems,
|
|
},
|
|
HTMLAttributes: {
|
|
class: "mention",
|
|
},
|
|
}).extend({
|
|
addNodeView() {
|
|
// Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191)
|
|
this.editor.isInitialized = true;
|
|
|
|
return ReactNodeViewRenderer(MentionView);
|
|
},
|
|
}),
|
|
CustomTable.configure({
|
|
resizable: true,
|
|
lastColumnResizable: true,
|
|
allowTableNodeSelection: true,
|
|
}),
|
|
TableRow,
|
|
TableCell,
|
|
TableHeader,
|
|
TableDndExtension,
|
|
MathInline.configure({
|
|
view: MathInlineView,
|
|
}),
|
|
MathBlock.configure({
|
|
view: MathBlockView,
|
|
}),
|
|
Details,
|
|
DetailsSummary,
|
|
DetailsContent,
|
|
Youtube.configure({
|
|
addPasteHandler: false,
|
|
controls: true,
|
|
nocookie: true,
|
|
}),
|
|
TiptapImage.configure({
|
|
view: ImageView,
|
|
allowBase64: false,
|
|
resize: {
|
|
enabled: true,
|
|
directions: ["left", "right"],
|
|
minWidth: 80,
|
|
minHeight: 40,
|
|
alwaysPreserveAspectRatio: true,
|
|
//@ts-ignore
|
|
createCustomHandle: createImageHandle,
|
|
className: imageResizeClasses,
|
|
},
|
|
}),
|
|
TiptapVideo.configure({
|
|
view: VideoView,
|
|
resize: {
|
|
enabled: true,
|
|
directions: ["left", "right"],
|
|
minWidth: 80,
|
|
minHeight: 40,
|
|
alwaysPreserveAspectRatio: true,
|
|
//@ts-ignore
|
|
createCustomHandle: createResizeHandle,
|
|
className: buildResizeClasses("node-video"),
|
|
},
|
|
}),
|
|
Callout.configure({
|
|
view: CalloutView,
|
|
}),
|
|
CustomCodeBlock.configure({
|
|
view: CodeBlockView,
|
|
//@ts-ignore
|
|
lowlight,
|
|
HTMLAttributes: {
|
|
spellcheck: false,
|
|
},
|
|
}),
|
|
Selection,
|
|
Attachment.configure({
|
|
view: AttachmentView,
|
|
}),
|
|
Drawio.configure({
|
|
view: DrawioView,
|
|
resize: {
|
|
enabled: true,
|
|
directions: ["left", "right"],
|
|
minWidth: 80,
|
|
minHeight: 40,
|
|
alwaysPreserveAspectRatio: true,
|
|
//@ts-ignore
|
|
createCustomHandle: createResizeHandle,
|
|
className: buildResizeClasses("node-drawio"),
|
|
},
|
|
}),
|
|
Excalidraw.configure({
|
|
view: ExcalidrawView,
|
|
resize: {
|
|
enabled: true,
|
|
directions: ["left", "right"],
|
|
minWidth: 80,
|
|
minHeight: 40,
|
|
alwaysPreserveAspectRatio: true,
|
|
//@ts-ignore
|
|
createCustomHandle: createResizeHandle,
|
|
className: buildResizeClasses("node-excalidraw"),
|
|
},
|
|
}),
|
|
Embed.configure({
|
|
view: EmbedView,
|
|
}),
|
|
Subpages.configure({
|
|
view: SubpagesView,
|
|
}),
|
|
MarkdownClipboard.configure({
|
|
transformPastedText: true,
|
|
}),
|
|
CharacterCount.configure({
|
|
wordCounter: (text) => countWords(text),
|
|
}),
|
|
SearchAndReplace.extend({
|
|
addKeyboardShortcuts() {
|
|
return {
|
|
"Mod-f": () => {
|
|
const event = new CustomEvent("openFindDialogFromEditor", {});
|
|
document.dispatchEvent(event);
|
|
return true;
|
|
},
|
|
Escape: () => {
|
|
const event = new CustomEvent("closeFindDialogFromEditor", {});
|
|
document.dispatchEvent(event);
|
|
return false;
|
|
},
|
|
};
|
|
},
|
|
}).configure(),
|
|
Columns,
|
|
Column,
|
|
] as any;
|
|
|
|
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
|
|
|
export const collabExtensions: CollabExtensions = (provider, user) => [
|
|
Collaboration.configure({
|
|
document: provider.document,
|
|
provider,
|
|
}),
|
|
CollaborationCaret.configure({
|
|
provider,
|
|
user: {
|
|
name: user.name,
|
|
color: randomElement(userColors),
|
|
},
|
|
}),
|
|
];
|